codex-agent-framework 0.1.26__tar.gz → 0.1.27__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.26/codex_agent_framework.egg-info → codex_agent_framework-0.1.27}/PKG-INFO +2 -1
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/README.md +1 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/context/__init__.py +12 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/files/__init__.py +3 -3
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/files/tools.py +14 -6
- codex_agent_framework-0.1.27/codex_agent/builtin_plugins/interface/__init__.py +53 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/status.py +83 -1
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/tui/chat.py +43 -6
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/utils.py +151 -19
- codex_agent_framework-0.1.27/codex_agent/version.py +1 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27/codex_agent_framework.egg-info}/PKG-INFO +2 -1
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/pyproject.toml +1 -1
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_agent.py +18 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_chat.py +89 -1
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_utils.py +64 -3
- codex_agent_framework-0.1.26/codex_agent/builtin_plugins/interface/__init__.py +0 -38
- codex_agent_framework-0.1.26/codex_agent/version.py +0 -1
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/CHANGELOG.md +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/LICENSE +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/MANIFEST.in +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/__init__.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/__main__.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/agent.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/agent_runtime.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/ai.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/browser.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_commands.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/__init__.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/bash/__init__.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/bash/requirements.txt +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/browser/__init__.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/browser/requirements.txt +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/content/__init__.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/content/requirements.txt +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/content/system.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/content/vision.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/desktop/__init__.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/desktop/requirements.txt +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/environment/__init__.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/environment/requirements.txt +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/files/requirements.txt +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/image_generation/__init__.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/image_generation/image_generation_system_prompt.txt +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/image_generation/requirements.txt +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/interface/requirements.txt +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/memory/__init__.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/memory/requirements.txt +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/planner/__init__.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/planner/requirements.txt +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/python/__init__.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/python/requirements.txt +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/realtime/__init__.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/realtime/audio.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/realtime/bridge.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/realtime/manager.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/realtime/requirements.txt +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/realtime/session.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/scheduler/__init__.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/scheduler/requirements.txt +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/subagents/__init__.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/subagents/profiles/explorer.json +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/subagents/requirements.txt +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/tts/__init__.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/builtin_plugins/tts/requirements.txt +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/cli/__init__.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/cli/headless.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/cli/main.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/cli/output.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/cli/root.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/cli/runner.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/client.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/command.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/config.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/context.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/desktop.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/event.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/get_text/__init__.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/get_text/default_gitignore +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/get_text/get_text.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/get_text/simpler_get_text.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/hooks.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/image.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/latex.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/mainloop.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/message.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/plugin.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/prompts/system_prompt.txt +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/provider.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/runtime.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/scheduler.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/scripts/install-system-dependencies.sh +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/server/__init__.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/server/app.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/server/core.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/service.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/sessions.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/stream.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/stream_utils.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/tool.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/tray.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/tui/__init__.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/tui/lifecycle.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent/worker.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent_framework.egg-info/SOURCES.txt +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent_framework.egg-info/dependency_links.txt +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent_framework.egg-info/entry_points.txt +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent_framework.egg-info/requires.txt +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/codex_agent_framework.egg-info/top_level.txt +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/dependencies.txt +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/setup.cfg +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_agent_runtime.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_ai.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_browser.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_builtin_config.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_cli.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_cli_headless.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_cli_root.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_client.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_events.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_get_text_browser.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_hooks.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_image_message.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_local_desktop.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_memory.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_messages.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_planner.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_scheduler.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_server.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_service.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_tray.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_tui.py +0 -0
- {codex_agent_framework-0.1.26 → codex_agent_framework-0.1.27}/tests/test_worker.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codex-agent-framework
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.27
|
|
4
4
|
Summary: A lightweight event-driven Codex agent runtime.
|
|
5
5
|
Author: Baptiste
|
|
6
6
|
License-Expression: MIT
|
|
@@ -626,6 +626,7 @@ Core LLM calls, memory embeddings, non-streaming STT transcription, and realtime
|
|
|
626
626
|
|
|
627
627
|
## Recent releases
|
|
628
628
|
|
|
629
|
+
- `0.1.27`: upgrade the agentic `read` tool with folder trees, recursive folder pattern search, and precise `start_at_line`/`end_at_line` ranges; add visible wrapper/token split stats, compactable turn counts, and refreshed TUI status-bar context telemetry.
|
|
629
630
|
- `0.1.26`: align SDK-facing config with Responses API shapes, add the `context` management plugin with selective non-destructive wrapper pruning, keep wrappers out of memory retrieval and compaction payloads, move embeddings/STT/realtime onto Codex backend auth, streamline voice/TTS plugin config, and improve TUI streaming/windowing.
|
|
630
631
|
- `0.1.25`: move built-in capabilities further into plugin-scoped modules, add bundled plugin requirement metadata, and replace the TUI conversation pane with a selectable rich chat area that preserves mouse scrolling, copy selection, lazy transcript loading, and bottom-aware auto-scroll.
|
|
631
632
|
- `0.1.24`: add slash-command lifecycle events and TUI progress/result rendering so commands such as `/compact` and `/help` show running, done, failed, result, and error feedback instead of going silent.
|
|
@@ -577,6 +577,7 @@ Core LLM calls, memory embeddings, non-streaming STT transcription, and realtime
|
|
|
577
577
|
|
|
578
578
|
## Recent releases
|
|
579
579
|
|
|
580
|
+
- `0.1.27`: upgrade the agentic `read` tool with folder trees, recursive folder pattern search, and precise `start_at_line`/`end_at_line` ranges; add visible wrapper/token split stats, compactable turn counts, and refreshed TUI status-bar context telemetry.
|
|
580
581
|
- `0.1.26`: align SDK-facing config with Responses API shapes, add the `context` management plugin with selective non-destructive wrapper pruning, keep wrappers out of memory retrieval and compaction payloads, move embeddings/STT/realtime onto Codex backend auth, streamline voice/TTS plugin config, and improve TUI streaming/windowing.
|
|
581
582
|
- `0.1.25`: move built-in capabilities further into plugin-scoped modules, add bundled plugin requirement metadata, and replace the TUI conversation pane with a selectable rich chat area that preserves mouse scrolling, copy selection, lazy transcript loading, and bottom-aware auto-scroll.
|
|
582
583
|
- `0.1.24`: add slash-command lifecycle events and TUI progress/result rendering so commands such as `/compact` and `/help` show running, done, failed, result, and error feedback instead of going silent.
|
|
@@ -33,6 +33,15 @@ Tool result wrappers expose stable `wrapper_id` values in their XML representati
|
|
|
33
33
|
total_history_messages=status.get("total_history_messages", 0),
|
|
34
34
|
persistent_session_messages=status.get("persistent_session_messages", 0),
|
|
35
35
|
temporary_session_messages=status.get("temporary_session_messages", 0),
|
|
36
|
+
active_wrapper_messages=status.get("active_wrapper_messages", 0),
|
|
37
|
+
vanished_wrapper_messages=status.get("vanished_wrapper_messages", 0),
|
|
38
|
+
total_wrapper_messages=status.get("total_wrapper_messages", 0),
|
|
39
|
+
base_context_tokens=status.get("base_context_tokens", status.used_tokens),
|
|
40
|
+
active_wrapper_tokens=status.get("active_wrapper_tokens", 0),
|
|
41
|
+
vanished_wrapper_tokens=status.get("vanished_wrapper_tokens", 0),
|
|
42
|
+
compactable_turns=status.get("compactable_turns", 0),
|
|
43
|
+
compactable_messages=status.get("compactable_messages", 0),
|
|
44
|
+
compactable_tokens=status.get("compactable_tokens", 0),
|
|
36
45
|
context_session_messages=status.get("context_session_messages", 0),
|
|
37
46
|
image_messages=status.get("image_messages", 0),
|
|
38
47
|
pending_messages=status.get("pending_messages", 0),
|
|
@@ -48,8 +57,11 @@ Tool result wrappers expose stable `wrapper_id` values in their XML representati
|
|
|
48
57
|
return "\n".join([
|
|
49
58
|
"Context status:",
|
|
50
59
|
f"- tokens: {status.used_tokens}/{status.input_token_limit} ({status.percent:.2f}%)",
|
|
60
|
+
f"- token split base/active_wrappers/vanished_wrappers: {status.base_context_tokens}/{status.active_wrapper_tokens}/{status.vanished_wrapper_tokens}",
|
|
51
61
|
f"- messages sent/total: {status.sent_session_messages}/{status.total_session_messages}",
|
|
52
62
|
f"- persistent/temporary context messages: {status.persistent_session_messages}/{status.temporary_session_messages}",
|
|
63
|
+
f"- wrappers active/vanished/total: {status.active_wrapper_messages}/{status.vanished_wrapper_messages}/{status.total_wrapper_messages}",
|
|
64
|
+
f"- compactable turns/messages/tokens: {status.compactable_turns}/{status.compactable_messages}/{status.compactable_tokens}",
|
|
53
65
|
f"- visible/total history messages: {status.visible_history_messages}/{status.total_history_messages}",
|
|
54
66
|
f"- tool spec tokens: {status.tool_spec_tokens}",
|
|
55
67
|
f"- auto_compact: {status.auto_compact} threshold={status.auto_compact_threshold}",
|
|
@@ -7,13 +7,13 @@ from . import tools as file_tools
|
|
|
7
7
|
|
|
8
8
|
class FilesPlugin(Plugin):
|
|
9
9
|
name = "files"
|
|
10
|
-
description = "Strict local UTF-8 file read
|
|
10
|
+
description = "Strict local UTF-8 file/folder read and search plus write/edit operations."
|
|
11
11
|
prefix_tools = False
|
|
12
12
|
system_prompt = """
|
|
13
13
|
# File Tools
|
|
14
14
|
|
|
15
|
-
Use the specialized file tools for local UTF-8 text
|
|
16
|
-
- `read`: read line-numbered local UTF-8 files precisely, with
|
|
15
|
+
Use the specialized file tools for local UTF-8 text operations:
|
|
16
|
+
- `read`: read line-numbered local UTF-8 files precisely, list local folders as a compact one-level tree, or search folders recursively with a pattern.
|
|
17
17
|
- `write`: create or overwrite complete UTF-8 text files; use with caution.
|
|
18
18
|
- `edit`: perform targeted exact-string replacements in local UTF-8 text files.
|
|
19
19
|
|
|
@@ -8,10 +8,11 @@ from ...utils import read_document_content, view_document_content
|
|
|
8
8
|
def read(reads):
|
|
9
9
|
"""
|
|
10
10
|
description: |
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
Read one or more local UTF-8 text files or folders as source-of-truth content.
|
|
12
|
+
File reads always prefix returned content with reliable source line numbers.
|
|
13
|
+
Folder reads without `pattern` render a compact tree with one level of recursion.
|
|
14
|
+
Folder reads with `pattern` recursively search UTF-8 files under the folder and return numbered snippets per matching file.
|
|
15
|
+
This tool does not extract document formats, fetch URLs, or inspect Python objects; use `view` for URLs, PDFs, DOCX, ODT, HTML, XLSX, archives, and other extracted or transformed content.
|
|
15
16
|
|
|
16
17
|
For large outputs: the output is automatically truncated. When truncated, a message indicates the omitted line range.
|
|
17
18
|
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.
|
|
@@ -20,16 +21,19 @@ def read(reads):
|
|
|
20
21
|
reads:
|
|
21
22
|
description: |
|
|
22
23
|
A single read object or a list of read objects.
|
|
23
|
-
Each object must contain `source` and may contain `start_at_line`, `pattern`, and `regex`.
|
|
24
|
+
Each object must contain `source` and may contain `start_at_line`, `end_at_line`, `pattern`, and `regex`.
|
|
24
25
|
anyOf:
|
|
25
26
|
- type: object
|
|
26
27
|
properties:
|
|
27
28
|
source:
|
|
28
|
-
description: Local UTF-8 text file path
|
|
29
|
+
description: Local UTF-8 text file or folder path. Folders are listed as a compact tree, or searched recursively when pattern is provided. For URLs, rich documents, archives, or Python objects, use `view` instead.
|
|
29
30
|
type: string
|
|
30
31
|
start_at_line:
|
|
31
32
|
description: Line number to start reading from (1-indexed). Default is 1 (start from beginning).
|
|
32
33
|
type: integer
|
|
34
|
+
end_at_line:
|
|
35
|
+
description: Optional inclusive line number to stop reading at for direct file reads.
|
|
36
|
+
type: integer
|
|
33
37
|
pattern:
|
|
34
38
|
description: Optional search pattern. Treated as a Python regular expression by default.
|
|
35
39
|
type: string
|
|
@@ -43,9 +47,12 @@ def read(reads):
|
|
|
43
47
|
type: object
|
|
44
48
|
properties:
|
|
45
49
|
source:
|
|
50
|
+
description: Local UTF-8 text file or folder path. Folders are listed as a compact tree, or searched recursively when pattern is provided.
|
|
46
51
|
type: string
|
|
47
52
|
start_at_line:
|
|
48
53
|
type: integer
|
|
54
|
+
end_at_line:
|
|
55
|
+
type: integer
|
|
49
56
|
pattern:
|
|
50
57
|
type: string
|
|
51
58
|
regex:
|
|
@@ -79,6 +86,7 @@ def apply_file_read(item):
|
|
|
79
86
|
return read_document_content(
|
|
80
87
|
source,
|
|
81
88
|
start_at_line=item.get("start_at_line", 1),
|
|
89
|
+
end_at_line=item.get("end_at_line"),
|
|
82
90
|
max_tokens=max_tokens,
|
|
83
91
|
pattern=item.get("pattern"),
|
|
84
92
|
regex=item.get("regex", True),
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"""Built-in local interface plugin."""
|
|
2
|
+
|
|
3
|
+
from ...plugin import Plugin
|
|
4
|
+
from ...provider import provider
|
|
5
|
+
from ...tool import tool
|
|
6
|
+
from ...tui import current_tui, open_tui_terminal, terminate_tui
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class InterfacePlugin(Plugin):
|
|
10
|
+
name = "interface"
|
|
11
|
+
description = "Open and close the local TUI interface."
|
|
12
|
+
prefix_tools = False
|
|
13
|
+
|
|
14
|
+
@provider
|
|
15
|
+
def tui_status(self):
|
|
16
|
+
"""Expose whether the local TUI client is currently open."""
|
|
17
|
+
tui = current_tui()
|
|
18
|
+
if not tui:
|
|
19
|
+
return "TUI status: closed."
|
|
20
|
+
return (
|
|
21
|
+
"TUI status: open. "
|
|
22
|
+
f"pid={tui.get('pid')} "
|
|
23
|
+
f"base_url={tui.get('base_url')} "
|
|
24
|
+
f"timestamp={tui.get('timestamp')}"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
@tool(name="open_tui")
|
|
28
|
+
def open_tui(self):
|
|
29
|
+
"""
|
|
30
|
+
description: Open the Textual chat TUI client in a separate terminal connected to the local agent server.
|
|
31
|
+
parameters: {}
|
|
32
|
+
required: []
|
|
33
|
+
"""
|
|
34
|
+
tui = current_tui()
|
|
35
|
+
if tui:
|
|
36
|
+
return {"status": "already_open", "tui": tui}
|
|
37
|
+
process = open_tui_terminal(base_url=self.agent.config.get("server_url", "http://127.0.0.1:8765"))
|
|
38
|
+
if process is None:
|
|
39
|
+
raise RuntimeError("No supported terminal emulator found to open the TUI")
|
|
40
|
+
return {"status": "opened", "pid": process.pid}
|
|
41
|
+
|
|
42
|
+
@tool(name="close_tui")
|
|
43
|
+
def close_tui(self):
|
|
44
|
+
"""
|
|
45
|
+
description: Close the currently registered Textual chat TUI client process.
|
|
46
|
+
parameters: {}
|
|
47
|
+
required: []
|
|
48
|
+
"""
|
|
49
|
+
tui = current_tui()
|
|
50
|
+
if not tui:
|
|
51
|
+
return {"status": "not_open"}
|
|
52
|
+
terminate_tui(tui.pid)
|
|
53
|
+
return {"status": "closed", "tui": tui}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from modict import modict
|
|
2
2
|
|
|
3
3
|
from .image import ImageMessage
|
|
4
|
+
from .message import ToolResultWrapper
|
|
4
5
|
from .utils import sort, truncate
|
|
5
6
|
|
|
6
7
|
|
|
@@ -32,6 +33,7 @@ class StatusManager:
|
|
|
32
33
|
max_images = self.agent.config.get("max_images", 10)
|
|
33
34
|
context_manager = self.agent.context_manager
|
|
34
35
|
context_manager.prune_orphaned_session_tool_outputs()
|
|
36
|
+
all_session_messages = list(self.agent.session.messages) if self.agent.session else []
|
|
35
37
|
session_messages = sort(
|
|
36
38
|
msg for msg in self.agent.session.context_messages()
|
|
37
39
|
if context_manager.is_message_visible_in_context(msg)
|
|
@@ -58,7 +60,9 @@ class StatusManager:
|
|
|
58
60
|
history_start = len(truncated_others) - i - 1
|
|
59
61
|
else:
|
|
60
62
|
break
|
|
61
|
-
|
|
63
|
+
included_history_start = max(0, history_start)
|
|
64
|
+
visible_history_count = len(truncated_others[included_history_start:])
|
|
65
|
+
included_others = others[included_history_start:]
|
|
62
66
|
sent_session_messages = visible_history_count + len(images)
|
|
63
67
|
total_session_messages = len(self.agent.session.messages) if self.agent.session else 0
|
|
64
68
|
persistent_session_messages = sum(
|
|
@@ -69,6 +73,39 @@ class StatusManager:
|
|
|
69
73
|
1 for msg in session_messages
|
|
70
74
|
if isinstance(msg.get("turns_left", -1), int) and msg.get("turns_left", -1) > 0
|
|
71
75
|
)
|
|
76
|
+
wrappers = [msg for msg in all_session_messages if isinstance(msg, ToolResultWrapper)]
|
|
77
|
+
active_wrappers = [
|
|
78
|
+
msg for msg in wrappers
|
|
79
|
+
if isinstance(msg.get("turns_left", -1), int) and msg.get("turns_left", -1) > 0
|
|
80
|
+
]
|
|
81
|
+
vanished_wrappers = [msg for msg in wrappers if msg.get("turns_left", -1) == 0]
|
|
82
|
+
|
|
83
|
+
def wrapper_token_count(messages):
|
|
84
|
+
total = 0
|
|
85
|
+
for msg in messages:
|
|
86
|
+
msg_copy = msg.copy()
|
|
87
|
+
if msg_copy.get("content") and isinstance(msg_copy.content, str):
|
|
88
|
+
msg_copy.content = truncate(msg_copy.content, max_tokens=max_input_tokens, model=self.agent.config.model)
|
|
89
|
+
total += context_manager.response_input_token_count([msg_copy], response_tools=[])
|
|
90
|
+
return total
|
|
91
|
+
|
|
92
|
+
active_context_wrappers = [msg for msg in included_others if isinstance(msg, ToolResultWrapper)]
|
|
93
|
+
active_wrapper_tokens = wrapper_token_count(active_context_wrappers)
|
|
94
|
+
vanished_wrapper_tokens = wrapper_token_count(vanished_wrappers)
|
|
95
|
+
base_context_tokens = max(0, current_count - active_wrapper_tokens)
|
|
96
|
+
if self.agent.session:
|
|
97
|
+
compaction_plan = self.agent.session.compaction_plan(
|
|
98
|
+
max_input_tokens=max_input_tokens,
|
|
99
|
+
model=self.agent.config.model,
|
|
100
|
+
max_compacted_turns=None,
|
|
101
|
+
)
|
|
102
|
+
compactable_turns = compaction_plan.completed_turn_count
|
|
103
|
+
compactable_messages = compaction_plan.compacted_message_count
|
|
104
|
+
compactable_tokens = compaction_plan.compacted_token_count
|
|
105
|
+
else:
|
|
106
|
+
compactable_turns = 0
|
|
107
|
+
compactable_messages = 0
|
|
108
|
+
compactable_tokens = 0
|
|
72
109
|
percent = (current_count / input_limit * 100) if input_limit else 0
|
|
73
110
|
status = modict(
|
|
74
111
|
used_tokens=current_count,
|
|
@@ -84,6 +121,15 @@ class StatusManager:
|
|
|
84
121
|
sent_session_message_percent=(sent_session_messages / total_session_messages * 100) if total_session_messages else 0,
|
|
85
122
|
persistent_session_messages=persistent_session_messages,
|
|
86
123
|
temporary_session_messages=temporary_session_messages,
|
|
124
|
+
active_wrapper_messages=len(active_wrappers),
|
|
125
|
+
vanished_wrapper_messages=len(vanished_wrappers),
|
|
126
|
+
total_wrapper_messages=len(wrappers),
|
|
127
|
+
base_context_tokens=base_context_tokens,
|
|
128
|
+
active_wrapper_tokens=active_wrapper_tokens,
|
|
129
|
+
vanished_wrapper_tokens=vanished_wrapper_tokens,
|
|
130
|
+
compactable_turns=compactable_turns,
|
|
131
|
+
compactable_messages=compactable_messages,
|
|
132
|
+
compactable_tokens=compactable_tokens,
|
|
87
133
|
context_session_messages=len(session_messages),
|
|
88
134
|
image_messages=len(images),
|
|
89
135
|
pending_messages=len(self.agent.mainloop_manager.pending),
|
|
@@ -116,6 +162,15 @@ class StatusManager:
|
|
|
116
162
|
sent_session_message_percent=estimated.sent_session_message_percent,
|
|
117
163
|
persistent_session_messages=estimated.get("persistent_session_messages", 0),
|
|
118
164
|
temporary_session_messages=estimated.get("temporary_session_messages", 0),
|
|
165
|
+
active_wrapper_messages=estimated.get("active_wrapper_messages", 0),
|
|
166
|
+
vanished_wrapper_messages=estimated.get("vanished_wrapper_messages", 0),
|
|
167
|
+
total_wrapper_messages=estimated.get("total_wrapper_messages", 0),
|
|
168
|
+
base_context_tokens=estimated.get("base_context_tokens", estimated.used_tokens),
|
|
169
|
+
active_wrapper_tokens=estimated.get("active_wrapper_tokens", 0),
|
|
170
|
+
vanished_wrapper_tokens=estimated.get("vanished_wrapper_tokens", 0),
|
|
171
|
+
compactable_turns=estimated.get("compactable_turns", 0),
|
|
172
|
+
compactable_messages=estimated.get("compactable_messages", 0),
|
|
173
|
+
compactable_tokens=estimated.get("compactable_tokens", 0),
|
|
119
174
|
quota_5h_percent=None,
|
|
120
175
|
quota_7d_percent=None,
|
|
121
176
|
model=self.agent.config.get("model") or "gpt-5.4",
|
|
@@ -143,6 +198,15 @@ class StatusManager:
|
|
|
143
198
|
sent_session_message_percent=estimated.sent_session_message_percent,
|
|
144
199
|
persistent_session_messages=estimated.get("persistent_session_messages", 0),
|
|
145
200
|
temporary_session_messages=estimated.get("temporary_session_messages", 0),
|
|
201
|
+
active_wrapper_messages=estimated.get("active_wrapper_messages", 0),
|
|
202
|
+
vanished_wrapper_messages=estimated.get("vanished_wrapper_messages", 0),
|
|
203
|
+
total_wrapper_messages=estimated.get("total_wrapper_messages", 0),
|
|
204
|
+
base_context_tokens=estimated.get("base_context_tokens", estimated.used_tokens),
|
|
205
|
+
active_wrapper_tokens=estimated.get("active_wrapper_tokens", 0),
|
|
206
|
+
vanished_wrapper_tokens=estimated.get("vanished_wrapper_tokens", 0),
|
|
207
|
+
compactable_turns=estimated.get("compactable_turns", 0),
|
|
208
|
+
compactable_messages=estimated.get("compactable_messages", 0),
|
|
209
|
+
compactable_tokens=estimated.get("compactable_tokens", 0),
|
|
146
210
|
model=self.agent.config.get("model") or "gpt-5.4",
|
|
147
211
|
busy=self.agent.mainloop_manager.busy,
|
|
148
212
|
current_turn_id=self.agent.mainloop_manager.current_turn_id,
|
|
@@ -167,6 +231,15 @@ class StatusManager:
|
|
|
167
231
|
sent_session_message_percent=estimated.sent_session_message_percent,
|
|
168
232
|
persistent_session_messages=estimated.get("persistent_session_messages", 0),
|
|
169
233
|
temporary_session_messages=estimated.get("temporary_session_messages", 0),
|
|
234
|
+
active_wrapper_messages=estimated.get("active_wrapper_messages", 0),
|
|
235
|
+
vanished_wrapper_messages=estimated.get("vanished_wrapper_messages", 0),
|
|
236
|
+
total_wrapper_messages=estimated.get("total_wrapper_messages", 0),
|
|
237
|
+
base_context_tokens=estimated.get("base_context_tokens", estimated.used_tokens),
|
|
238
|
+
active_wrapper_tokens=estimated.get("active_wrapper_tokens", 0),
|
|
239
|
+
vanished_wrapper_tokens=estimated.get("vanished_wrapper_tokens", 0),
|
|
240
|
+
compactable_turns=estimated.get("compactable_turns", 0),
|
|
241
|
+
compactable_messages=estimated.get("compactable_messages", 0),
|
|
242
|
+
compactable_tokens=estimated.get("compactable_tokens", 0),
|
|
170
243
|
model=self.agent.config.get("model") or "gpt-5.4",
|
|
171
244
|
busy=self.agent.mainloop_manager.busy,
|
|
172
245
|
current_turn_id=self.agent.mainloop_manager.current_turn_id,
|
|
@@ -193,6 +266,15 @@ class StatusManager:
|
|
|
193
266
|
sent_session_message_percent=context_status.get("sent_session_message_percent", 0),
|
|
194
267
|
persistent_session_messages=context_status.get("persistent_session_messages", 0),
|
|
195
268
|
temporary_session_messages=context_status.get("temporary_session_messages", 0),
|
|
269
|
+
active_wrapper_messages=context_status.get("active_wrapper_messages", 0),
|
|
270
|
+
vanished_wrapper_messages=context_status.get("vanished_wrapper_messages", 0),
|
|
271
|
+
total_wrapper_messages=context_status.get("total_wrapper_messages", 0),
|
|
272
|
+
base_context_tokens=context_status.get("base_context_tokens", max(0, input_tokens)),
|
|
273
|
+
active_wrapper_tokens=context_status.get("active_wrapper_tokens", 0),
|
|
274
|
+
vanished_wrapper_tokens=context_status.get("vanished_wrapper_tokens", 0),
|
|
275
|
+
compactable_turns=context_status.get("compactable_turns", 0),
|
|
276
|
+
compactable_messages=context_status.get("compactable_messages", 0),
|
|
277
|
+
compactable_tokens=context_status.get("compactable_tokens", 0),
|
|
196
278
|
quota_5h_percent=wham.quota_5h_percent,
|
|
197
279
|
quota_7d_percent=wham.quota_7d_percent,
|
|
198
280
|
model=model or self.agent.config.get("model") or "gpt-5.4",
|
|
@@ -801,20 +801,34 @@ class Chat:
|
|
|
801
801
|
context_style = cls._percent_style(percent)
|
|
802
802
|
used = cls._format_ktok(status.get("context_current_tokens") or 0)
|
|
803
803
|
limit = cls._format_ktok(status.get("context_limit_tokens") or 0)
|
|
804
|
+
context_segments = cls._context_token_status_segments(status, used, limit)
|
|
804
805
|
quota_5h = cls._format_percent(status.get("quota_5h_percent"))
|
|
805
806
|
quota_7d = cls._format_percent(status.get("quota_7d_percent"))
|
|
806
807
|
sent_messages = int(status.get("sent_session_messages") or 0)
|
|
807
808
|
persistent_messages = status.get("persistent_session_messages")
|
|
808
809
|
temporary_messages = status.get("temporary_session_messages")
|
|
810
|
+
active_wrapper_messages = status.get("active_wrapper_messages")
|
|
811
|
+
vanished_wrapper_messages = status.get("vanished_wrapper_messages")
|
|
809
812
|
total_messages = int(status.get("total_session_messages") or 0)
|
|
813
|
+
compactable_turns = int(status.get("compactable_turns") or 0)
|
|
810
814
|
model = status.get("model") or "-"
|
|
811
|
-
message_segments = cls._message_status_segments(
|
|
815
|
+
message_segments = cls._message_status_segments(
|
|
816
|
+
sent_messages,
|
|
817
|
+
persistent_messages,
|
|
818
|
+
temporary_messages,
|
|
819
|
+
total_messages,
|
|
820
|
+
active_wrapper_messages=active_wrapper_messages,
|
|
821
|
+
vanished_wrapper_messages=vanished_wrapper_messages,
|
|
822
|
+
)
|
|
812
823
|
return Text.assemble(
|
|
813
824
|
(" ctx ", "bold #c9d1d9 on #161b22"),
|
|
814
|
-
|
|
825
|
+
*context_segments,
|
|
826
|
+
(" ", "#8b949e on #161b22"),
|
|
815
827
|
(f"{percent:.1f}%", f"{context_style} on #161b22"),
|
|
816
828
|
(" msg ", "#6e7681 on #161b22"),
|
|
817
829
|
*message_segments,
|
|
830
|
+
(" cmp ", "#6e7681 on #161b22"),
|
|
831
|
+
(str(compactable_turns), "#c9d1d9 on #161b22"),
|
|
818
832
|
(" 5h ", "#6e7681 on #161b22"),
|
|
819
833
|
(quota_5h, f"{cls._percent_style(status.get('quota_5h_percent'))} on #161b22"),
|
|
820
834
|
(" 7d ", "#6e7681 on #161b22"),
|
|
@@ -823,18 +837,41 @@ class Chat:
|
|
|
823
837
|
(str(model), "#c9d1d9 on #161b22"),
|
|
824
838
|
)
|
|
825
839
|
|
|
840
|
+
@classmethod
|
|
841
|
+
def _context_token_status_segments(cls, status, used, limit):
|
|
842
|
+
if "base_context_tokens" not in status and "active_wrapper_tokens" not in status and "vanished_wrapper_tokens" not in status:
|
|
843
|
+
return ((f"{used}/{limit} ktok", "#8b949e on #161b22"),)
|
|
844
|
+
base = cls._format_ktok(status.get("base_context_tokens") or 0)
|
|
845
|
+
active_wrappers = cls._format_ktok(status.get("active_wrapper_tokens") or 0)
|
|
846
|
+
vanished_wrappers = cls._format_ktok(status.get("vanished_wrapper_tokens") or 0)
|
|
847
|
+
return (
|
|
848
|
+
(base, "#c9d1d9 on #161b22"),
|
|
849
|
+
(f"+{active_wrappers}", "italic #f0883e on #161b22"),
|
|
850
|
+
(f"+{vanished_wrappers}", "italic #6e7681 on #161b22"),
|
|
851
|
+
(f"/{limit} ktok", "#8b949e on #161b22"),
|
|
852
|
+
)
|
|
853
|
+
|
|
826
854
|
@staticmethod
|
|
827
|
-
def _message_status_segments(sent_messages, persistent_messages, temporary_messages, total_messages):
|
|
828
|
-
if persistent_messages is None and temporary_messages is None:
|
|
855
|
+
def _message_status_segments(sent_messages, persistent_messages, temporary_messages, total_messages, *, active_wrapper_messages=None, vanished_wrapper_messages=None):
|
|
856
|
+
if persistent_messages is None and temporary_messages is None and active_wrapper_messages is None and vanished_wrapper_messages is None:
|
|
829
857
|
return ((f"{sent_messages}/{total_messages}", "#c9d1d9 on #161b22"),)
|
|
830
858
|
try:
|
|
831
859
|
persistent = int(persistent_messages or 0)
|
|
832
|
-
|
|
860
|
+
if active_wrapper_messages is None and vanished_wrapper_messages is None:
|
|
861
|
+
temporary = int(temporary_messages or 0)
|
|
862
|
+
return (
|
|
863
|
+
(str(persistent), "#c9d1d9 on #161b22"),
|
|
864
|
+
(f"+{temporary}", "italic #f0883e on #161b22"),
|
|
865
|
+
(f"/{total_messages}", "#c9d1d9 on #161b22"),
|
|
866
|
+
)
|
|
867
|
+
active_wrappers = int(active_wrapper_messages or 0)
|
|
868
|
+
vanished_wrappers = int(vanished_wrapper_messages or 0)
|
|
833
869
|
except Exception:
|
|
834
870
|
return ((f"{sent_messages}/{total_messages}", "#c9d1d9 on #161b22"),)
|
|
835
871
|
return (
|
|
836
872
|
(str(persistent), "#c9d1d9 on #161b22"),
|
|
837
|
-
(f"+{
|
|
873
|
+
(f"+{active_wrappers}", "italic #f0883e on #161b22"),
|
|
874
|
+
(f"+{vanished_wrappers}", "italic #6e7681 on #161b22"),
|
|
838
875
|
(f"/{total_messages}", "#c9d1d9 on #161b22"),
|
|
839
876
|
)
|
|
840
877
|
|
|
@@ -105,18 +105,21 @@ def add_line_numbers(text: str, start_line: int = 1, total_lines: int | None = N
|
|
|
105
105
|
return _number_lines(text.splitlines(), start_line=start_line, total_lines=total_lines)
|
|
106
106
|
|
|
107
107
|
|
|
108
|
-
def
|
|
108
|
+
def pattern_line_numbers(text: str, pattern, regex_pattern: bool = True) -> list[int]:
|
|
109
109
|
pattern_text = str(pattern)
|
|
110
110
|
lines = text.splitlines()
|
|
111
|
-
total_lines = len(lines)
|
|
112
111
|
if regex_pattern:
|
|
113
112
|
try:
|
|
114
113
|
compiled_pattern = re.compile(pattern_text)
|
|
115
114
|
except re.error as exc:
|
|
116
115
|
raise ValueError(f"Invalid regex pattern {pattern_text!r}: {exc}") from exc
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
116
|
+
return [index for index, line in enumerate(lines, start=1) if compiled_pattern.search(line)]
|
|
117
|
+
return [index for index, line in enumerate(lines, start=1) if pattern_text in line]
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _numbered_snippets_for_matches(text: str, matches: list[int], context_lines: int = 10) -> str:
|
|
121
|
+
lines = text.splitlines()
|
|
122
|
+
total_lines = len(lines)
|
|
120
123
|
if not matches:
|
|
121
124
|
return _number_lines(lines, start_line=1, total_lines=total_lines)
|
|
122
125
|
|
|
@@ -139,6 +142,14 @@ def pattern_snippets(text: str, pattern, context_lines: int = 10, regex_pattern:
|
|
|
139
142
|
)
|
|
140
143
|
return "\n\n".join(parts)
|
|
141
144
|
|
|
145
|
+
|
|
146
|
+
def pattern_snippets(text: str, pattern, context_lines: int = 10, regex_pattern: bool = True) -> str:
|
|
147
|
+
return _numbered_snippets_for_matches(
|
|
148
|
+
text,
|
|
149
|
+
pattern_line_numbers(text, pattern, regex_pattern=regex_pattern),
|
|
150
|
+
context_lines=context_lines,
|
|
151
|
+
)
|
|
152
|
+
|
|
142
153
|
def short_id(length=8):
|
|
143
154
|
return "".join(random.choices(string.ascii_letters,k=length))
|
|
144
155
|
|
|
@@ -461,14 +472,45 @@ class Logger:
|
|
|
461
472
|
with open(file,'w',encoding="utf-8") as f:
|
|
462
473
|
f.write(str(content)+'\n')
|
|
463
474
|
|
|
464
|
-
def _render_numbered_read_text(text: str, start_at_line=1, max_tokens=8000, pattern=None, pattern_context_lines=10, regex=True):
|
|
465
|
-
effective_start_at_line = int(start_at_line or 1)
|
|
475
|
+
def _render_numbered_read_text(text: str, start_at_line=1, end_at_line=None, max_tokens=8000, pattern=None, pattern_context_lines=10, regex=True):
|
|
476
|
+
effective_start_at_line = max(1, int(start_at_line or 1))
|
|
466
477
|
total_lines = len(text.splitlines())
|
|
467
478
|
if pattern:
|
|
468
479
|
text = pattern_snippets(text, pattern, context_lines=pattern_context_lines, regex_pattern=regex)
|
|
469
480
|
if max_tokens > 0:
|
|
470
481
|
text = truncate(text, max_tokens=max_tokens, start_line=1)
|
|
471
482
|
return text
|
|
483
|
+
|
|
484
|
+
if end_at_line is not None:
|
|
485
|
+
effective_end_at_line = int(end_at_line)
|
|
486
|
+
if effective_end_at_line < effective_start_at_line:
|
|
487
|
+
raise ValueError("end_at_line must be greater than or equal to start_at_line")
|
|
488
|
+
lines = text.splitlines(keepends=True)
|
|
489
|
+
if effective_start_at_line > len(lines):
|
|
490
|
+
text = ""
|
|
491
|
+
else:
|
|
492
|
+
selected_lines = lines[effective_start_at_line - 1:effective_end_at_line]
|
|
493
|
+
if max_tokens > 0:
|
|
494
|
+
result_lines = []
|
|
495
|
+
current_tokens = 0
|
|
496
|
+
for line in selected_lines:
|
|
497
|
+
line_tokens = token_count(line)
|
|
498
|
+
if current_tokens + line_tokens <= max_tokens:
|
|
499
|
+
result_lines.append(line)
|
|
500
|
+
current_tokens += line_tokens
|
|
501
|
+
else:
|
|
502
|
+
break
|
|
503
|
+
text = ''.join(result_lines)
|
|
504
|
+
if len(result_lines) < len(selected_lines):
|
|
505
|
+
if not text.endswith('\n'):
|
|
506
|
+
text += '\n'
|
|
507
|
+
first_truncated = effective_start_at_line + len(result_lines)
|
|
508
|
+
last_truncated = effective_start_at_line + len(selected_lines) - 1
|
|
509
|
+
text += f"\n...\n\n[Lines {first_truncated}-{last_truncated} truncated]\n"
|
|
510
|
+
else:
|
|
511
|
+
text = ''.join(selected_lines)
|
|
512
|
+
return add_line_numbers(text, start_line=effective_start_at_line, total_lines=total_lines)
|
|
513
|
+
|
|
472
514
|
# Apply pagination and truncation - truncate function handles smart chunking internally
|
|
473
515
|
if max_tokens > 0:
|
|
474
516
|
text = truncate(text, max_tokens=max_tokens, start_line=effective_start_at_line)
|
|
@@ -502,24 +544,114 @@ def _view_search_snippets(text: str, search_query, context_lines: int = 10) -> s
|
|
|
502
544
|
return "\n\n".join(parts)
|
|
503
545
|
|
|
504
546
|
|
|
505
|
-
|
|
506
|
-
""
|
|
547
|
+
READ_DIRECTORY_EXCLUDES = {
|
|
548
|
+
".git",
|
|
549
|
+
".hg",
|
|
550
|
+
".svn",
|
|
551
|
+
".mypy_cache",
|
|
552
|
+
".pytest_cache",
|
|
553
|
+
".ruff_cache",
|
|
554
|
+
".tox",
|
|
555
|
+
".venv",
|
|
556
|
+
"__pycache__",
|
|
557
|
+
"build",
|
|
558
|
+
"dist",
|
|
559
|
+
"node_modules",
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
def _directory_entries(path):
|
|
564
|
+
try:
|
|
565
|
+
entries = list(os.scandir(path))
|
|
566
|
+
except OSError as exc:
|
|
567
|
+
raise OSError(f"failed to list directory {path}: {exc}") from exc
|
|
568
|
+
entries = [entry for entry in entries if entry.name not in READ_DIRECTORY_EXCLUDES]
|
|
569
|
+
return sorted(entries, key=lambda entry: (not entry.is_dir(follow_symlinks=False), entry.name.lower()))
|
|
570
|
+
|
|
571
|
+
|
|
572
|
+
def _render_directory_tree(path, max_tokens=8000):
|
|
573
|
+
root = os.path.abspath(path)
|
|
574
|
+
lines = [f"{os.path.basename(root) or root}/"]
|
|
575
|
+
entries = _directory_entries(root)
|
|
576
|
+
for index, entry in enumerate(entries):
|
|
577
|
+
is_last = index == len(entries) - 1
|
|
578
|
+
connector = "└── " if is_last else "├── "
|
|
579
|
+
child_prefix = " " if is_last else "│ "
|
|
580
|
+
suffix = "/" if entry.is_dir(follow_symlinks=False) else ""
|
|
581
|
+
lines.append(f"{connector}{entry.name}{suffix}")
|
|
582
|
+
if entry.is_dir(follow_symlinks=False):
|
|
583
|
+
children = _directory_entries(entry.path)
|
|
584
|
+
for child_index, child in enumerate(children):
|
|
585
|
+
child_connector = "└── " if child_index == len(children) - 1 else "├── "
|
|
586
|
+
child_suffix = "/" if child.is_dir(follow_symlinks=False) else ""
|
|
587
|
+
lines.append(f"{child_prefix}{child_connector}{child.name}{child_suffix}")
|
|
588
|
+
text = "\n".join(lines)
|
|
589
|
+
if max_tokens > 0:
|
|
590
|
+
text = truncate(text, max_tokens=max_tokens, start_line=1)
|
|
591
|
+
return text
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
def _iter_directory_files(path):
|
|
595
|
+
for root, dirnames, filenames in os.walk(path):
|
|
596
|
+
dirnames[:] = sorted(
|
|
597
|
+
[dirname for dirname in dirnames if dirname not in READ_DIRECTORY_EXCLUDES],
|
|
598
|
+
key=str.lower,
|
|
599
|
+
)
|
|
600
|
+
for filename in sorted(filenames, key=str.lower):
|
|
601
|
+
yield os.path.join(root, filename)
|
|
602
|
+
|
|
603
|
+
|
|
604
|
+
def _read_utf8_text_file(path):
|
|
605
|
+
with open(path, "rb") as f:
|
|
606
|
+
data = f.read()
|
|
607
|
+
try:
|
|
608
|
+
return data.decode("utf-8")
|
|
609
|
+
except UnicodeDecodeError as exc:
|
|
610
|
+
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
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
def _directory_pattern_search(path, pattern, max_tokens=8000, pattern_context_lines=10, regex=True):
|
|
614
|
+
pattern_text = str(pattern)
|
|
615
|
+
parts = []
|
|
616
|
+
skipped = 0
|
|
617
|
+
for file_path in _iter_directory_files(path):
|
|
618
|
+
try:
|
|
619
|
+
text = _read_utf8_text_file(file_path)
|
|
620
|
+
except UnicodeDecodeError:
|
|
621
|
+
skipped += 1
|
|
622
|
+
continue
|
|
623
|
+
matches = pattern_line_numbers(text, pattern_text, regex_pattern=regex)
|
|
624
|
+
if not matches:
|
|
625
|
+
continue
|
|
626
|
+
snippet = _numbered_snippets_for_matches(text, matches, context_lines=pattern_context_lines)
|
|
627
|
+
rel_path = os.path.relpath(file_path, path)
|
|
628
|
+
parts.append(f"--- file: {rel_path} ---\n{snippet}")
|
|
629
|
+
if not parts:
|
|
630
|
+
result = f"No matches for pattern: {pattern_text!r}"
|
|
631
|
+
else:
|
|
632
|
+
result = "\n\n".join(parts)
|
|
633
|
+
if skipped:
|
|
634
|
+
result += f"\n\n[Skipped {skipped} non-UTF-8 file(s)]"
|
|
635
|
+
if max_tokens > 0:
|
|
636
|
+
result = truncate(result, max_tokens=max_tokens, start_line=1)
|
|
637
|
+
return result
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def read_document_content(source, start_at_line=1, end_at_line=None, max_tokens=8000, pattern=None, pattern_context_lines=10, regex=True):
|
|
641
|
+
"""Read local UTF-8 files, list directories, or recursively search directories by pattern."""
|
|
507
642
|
if not isinstance(source, (str, os.PathLike)):
|
|
508
|
-
raise TypeError("read only accepts a local file path. Use view for objects or extracted document content.")
|
|
643
|
+
raise TypeError("read only accepts a local file or directory path. Use view for objects or extracted document content.")
|
|
509
644
|
path = os.fspath(source)
|
|
510
645
|
if path.startswith(("http://", "https://")):
|
|
511
|
-
raise ValueError("read only accepts local UTF-8
|
|
646
|
+
raise ValueError("read only accepts local UTF-8 files or directories. Use view for URLs.")
|
|
512
647
|
if os.path.isdir(path):
|
|
513
|
-
|
|
648
|
+
if pattern:
|
|
649
|
+
return _directory_pattern_search(path, pattern, max_tokens=max_tokens, pattern_context_lines=pattern_context_lines, regex=regex)
|
|
650
|
+
return _render_directory_tree(path, max_tokens=max_tokens)
|
|
514
651
|
if not os.path.isfile(path):
|
|
515
652
|
raise FileNotFoundError(f"File not found: {path}")
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
try:
|
|
519
|
-
text = data.decode("utf-8")
|
|
520
|
-
except UnicodeDecodeError as exc:
|
|
521
|
-
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
|
|
522
|
-
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)
|
|
653
|
+
text = _read_utf8_text_file(path)
|
|
654
|
+
return _render_numbered_read_text(text, start_at_line=start_at_line, end_at_line=end_at_line, max_tokens=max_tokens, pattern=pattern, pattern_context_lines=pattern_context_lines, regex=regex)
|
|
523
655
|
|
|
524
656
|
|
|
525
657
|
def view_document_content(source, start_at_line=1, max_tokens=8000, search_query=None, search_context_lines=10):
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.27"
|