codex-agent-framework 0.1.55__tar.gz → 0.1.57__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.55 → codex_agent_framework-0.1.57}/CHANGELOG.md +21 -0
- {codex_agent_framework-0.1.55/codex_agent_framework.egg-info → codex_agent_framework-0.1.57}/PKG-INFO +1 -1
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/client.py +34 -4
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/server/app.py +26 -3
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/stream_utils.py +42 -75
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/tui/app.py +72 -45
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/tui/chat.py +4 -1
- codex_agent_framework-0.1.57/codex_agent/tui/perf.py +73 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/tui/state.py +34 -2
- codex_agent_framework-0.1.57/codex_agent/tui/stream_debug.py +84 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/tui/style.py +49 -0
- codex_agent_framework-0.1.57/codex_agent/tui/widgets.py +248 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/utils.py +30 -57
- codex_agent_framework-0.1.57/codex_agent/version.py +1 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57/codex_agent_framework.egg-info}/PKG-INFO +1 -1
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent_framework.egg-info/SOURCES.txt +3 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/pyproject.toml +1 -1
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_chat.py +47 -58
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_server.py +50 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_utils.py +87 -1
- codex_agent_framework-0.1.55/codex_agent/version.py +0 -1
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/LICENSE +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/MANIFEST.in +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/README.md +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/__init__.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/__main__.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/agent.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/agent_runtime.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/ai.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/browser.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_commands.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/__init__.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/bash/__init__.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/bash/requirements.txt +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/browser/__init__.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/browser/requirements.txt +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/content/__init__.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/content/requirements.txt +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/content/system.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/content/vision.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/context/__init__.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/desktop/__init__.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/desktop/requirements.txt +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/environment/__init__.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/environment/requirements.txt +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/files/__init__.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/files/requirements.txt +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/files/tools.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/image_generation/__init__.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/image_generation/image_generation_system_prompt.txt +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/image_generation/requirements.txt +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/interface/__init__.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/interface/requirements.txt +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/memory/__init__.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/memory/requirements.txt +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/planner/__init__.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/planner/requirements.txt +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/python/__init__.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/python/requirements.txt +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/realtime/__init__.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/realtime/audio.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/realtime/bridge.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/realtime/manager.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/realtime/requirements.txt +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/realtime/session.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/scheduler/__init__.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/scheduler/requirements.txt +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/subagents/__init__.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/subagents/profiles/explorer.json +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/subagents/requirements.txt +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/tts/__init__.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/tts/requirements.txt +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/builtin_plugins/web_search/__init__.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/cli/__init__.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/cli/headless.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/cli/main.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/cli/output.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/cli/root.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/cli/runner.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/command.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/config.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/context.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/desktop.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/event.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/get_text/__init__.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/get_text/default_gitignore +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/get_text/get_text.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/get_text/simpler_get_text.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/hooks.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/image.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/latex.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/mainloop.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/message.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/plugin.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/prompts/system_prompt.txt +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/provider.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/runtime.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/scheduler.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/scripts/install-system-dependencies.sh +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/server/__init__.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/server/core.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/service.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/sessions.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/status.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/stream.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/tool.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/tray.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/tui/__init__.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/tui/lifecycle.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/tui/log.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/tui/status_bar.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/tui/transcript.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent/worker.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent_framework.egg-info/dependency_links.txt +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent_framework.egg-info/entry_points.txt +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent_framework.egg-info/requires.txt +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/codex_agent_framework.egg-info/top_level.txt +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/dependencies.txt +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/setup.cfg +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_agent.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_agent_audio_hooks.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_agent_commands.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_agent_context.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_agent_desktop.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_agent_environment.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_agent_image_generation.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_agent_observe.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_agent_plugins.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_agent_providers_config.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_agent_realtime.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_agent_response.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_agent_runtime.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_agent_sessions.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_agent_subagents.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_agent_tools.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_agent_turns.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_ai.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_browser.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_builtin_config.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_cli.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_cli_headless.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_cli_root.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_client.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_events.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_get_text_browser.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_hooks.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_image_message.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_local_desktop.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_memory.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_messages.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_planner.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_scheduler.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_service.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_tui.py +0 -0
- {codex_agent_framework-0.1.55 → codex_agent_framework-0.1.57}/tests/test_worker.py +0 -0
|
@@ -4,6 +4,27 @@ 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
6
|
|
|
7
|
+
## [0.1.57] - 2026-05-17
|
|
8
|
+
### Fixed
|
|
9
|
+
- Fix stream helper fan-out edge cases so `Tee`, `Splitter`, and `MappingStreamSplitter` readers keep their own queues and reusable mapping splitters do not redirect old readers to new streams.
|
|
10
|
+
- Make `utils.Logger` create its parent directory before truncating the log file so first use works on fresh paths.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Simplify small utility helpers for line-preserving truncation, message packing, and fenced stream pattern construction while preserving their existing behavior.
|
|
14
|
+
|
|
15
|
+
### Tests
|
|
16
|
+
- Add focused regression coverage for the stream helpers, logger initialization, and utility formatting/truncation helpers; validate with the full offline CI suite (580 passed).
|
|
17
|
+
|
|
18
|
+
## [0.1.56] - 2026-05-17
|
|
19
|
+
### Changed
|
|
20
|
+
- Replace the TUI chat surface with a widget-based timeline inspired by Textual chat patterns so live assistant output updates an active message widget instead of mutating a monolithic transcript buffer.
|
|
21
|
+
- Add compact TUI event streams that truncate verbose tool/slash result payloads for live UI subscribers while preserving full replay/API data outside compact mode.
|
|
22
|
+
- Add persistent TUI stream diagnostics in `~/.codex-agent/tui-stream-debug.jsonl` and lightweight perf snapshots in `~/.codex-agent/tui-perf.json` to capture intermittent live-stream stalls after the fact.
|
|
23
|
+
- Preserve the previous sparse terminal look with role headers, compact tool/info/error rows, a right-aligned step marker, and sticky-only autoscroll.
|
|
24
|
+
|
|
25
|
+
### Tests
|
|
26
|
+
- Validate the TUI/server/client changes with targeted chat/client/server coverage and the full offline CI suite (570 passed).
|
|
27
|
+
|
|
7
28
|
## [0.1.55] - 2026-05-17
|
|
8
29
|
### Fixed
|
|
9
30
|
- Decouple SSE event-stream consumption from local client/TUI event handling so slow or blocked UI work no longer stalls event intake during long-running or heavy tool activity.
|
|
@@ -14,11 +14,12 @@ from .utils import snake_case_type
|
|
|
14
14
|
class AgentClient:
|
|
15
15
|
"""Small HTTP/SSE client for a codex-agent server."""
|
|
16
16
|
|
|
17
|
-
def __init__(self, base_url="http://127.0.0.1:8765", timeout=10, event_read_timeout=15.0, event_stale_after=5.0):
|
|
17
|
+
def __init__(self, base_url="http://127.0.0.1:8765", timeout=10, event_read_timeout=15.0, event_stale_after=5.0, compact_events=False):
|
|
18
18
|
self.base_url = base_url.rstrip("/")
|
|
19
19
|
self.timeout = timeout
|
|
20
20
|
self.event_read_timeout = event_read_timeout
|
|
21
21
|
self.event_stale_after = event_stale_after
|
|
22
|
+
self.compact_events = compact_events
|
|
22
23
|
self.client_pid = os.getpid()
|
|
23
24
|
self._event_handlers = {}
|
|
24
25
|
self._event_thread = None
|
|
@@ -36,6 +37,16 @@ class AgentClient:
|
|
|
36
37
|
self.health_interval = 1.0
|
|
37
38
|
self.health_failure_threshold = 3
|
|
38
39
|
self._health_failures = 0
|
|
40
|
+
self.stream_debug = None
|
|
41
|
+
|
|
42
|
+
def debug_stream(self, stage, event=None, **fields):
|
|
43
|
+
logger = getattr(self, "stream_debug", None)
|
|
44
|
+
if logger is None:
|
|
45
|
+
return None
|
|
46
|
+
try:
|
|
47
|
+
return logger.record(stage, event, **fields)
|
|
48
|
+
except Exception:
|
|
49
|
+
return None
|
|
39
50
|
|
|
40
51
|
def on(self, event_type, handler=None):
|
|
41
52
|
if handler is None:
|
|
@@ -73,9 +84,13 @@ class AgentClient:
|
|
|
73
84
|
seen.add(handler)
|
|
74
85
|
handlers.append(handler)
|
|
75
86
|
for handler in handlers:
|
|
87
|
+
handler_name = getattr(handler, "__name__", handler.__class__.__name__)
|
|
88
|
+
self.debug_stream("handler_begin", event, handler=handler_name)
|
|
76
89
|
try:
|
|
77
90
|
handler(event)
|
|
91
|
+
self.debug_stream("handler_done", event, handler=handler_name)
|
|
78
92
|
except Exception as exc:
|
|
93
|
+
self.debug_stream("handler_error", event, handler=handler_name, error=str(exc))
|
|
79
94
|
if event_type != "client_event_error":
|
|
80
95
|
self.emit_local({"type": "client_event_error", "error": str(exc), "event": event_type})
|
|
81
96
|
return event
|
|
@@ -89,7 +104,9 @@ class AgentClient:
|
|
|
89
104
|
|
|
90
105
|
def _enqueue_local(self, event):
|
|
91
106
|
self._start_dispatch_thread()
|
|
92
|
-
|
|
107
|
+
event = modict(event)
|
|
108
|
+
self.debug_stream("queue_put", event, queue_size=self._local_event_queue.qsize())
|
|
109
|
+
self._local_event_queue.put(event)
|
|
93
110
|
return event
|
|
94
111
|
|
|
95
112
|
def _dispatch_loop(self):
|
|
@@ -102,7 +119,13 @@ class AgentClient:
|
|
|
102
119
|
continue
|
|
103
120
|
if event is None:
|
|
104
121
|
break
|
|
105
|
-
self.
|
|
122
|
+
self.debug_stream("dispatch_begin", event, queue_size=self._local_event_queue.qsize())
|
|
123
|
+
try:
|
|
124
|
+
self.emit_local(event)
|
|
125
|
+
self.debug_stream("dispatch_done", event, queue_size=self._local_event_queue.qsize())
|
|
126
|
+
except Exception as exc:
|
|
127
|
+
self.debug_stream("dispatch_error", event, error=str(exc), queue_size=self._local_event_queue.qsize())
|
|
128
|
+
raise
|
|
106
129
|
|
|
107
130
|
def start_events(self):
|
|
108
131
|
if self._event_thread and self._event_thread.is_alive():
|
|
@@ -226,11 +249,14 @@ class AgentClient:
|
|
|
226
249
|
self._set_server_connected()
|
|
227
250
|
stream_ended = True
|
|
228
251
|
try:
|
|
229
|
-
for
|
|
252
|
+
# Keep SSE latency low for interactive streams without
|
|
253
|
+
# falling back to the old byte-by-byte reader.
|
|
254
|
+
for event in self.iter_sse(response.iter_lines(chunk_size=16, decode_unicode=True)):
|
|
230
255
|
if self._event_stop.is_set():
|
|
231
256
|
stream_ended = False
|
|
232
257
|
break
|
|
233
258
|
self._record_event_stream_activity()
|
|
259
|
+
self.debug_stream("sse_received", event)
|
|
234
260
|
self.remember_event_sequence(event)
|
|
235
261
|
self._enqueue_local(event)
|
|
236
262
|
finally:
|
|
@@ -324,6 +350,8 @@ class AgentClient:
|
|
|
324
350
|
self._last_event_sequence = sequence
|
|
325
351
|
def events_url(self):
|
|
326
352
|
params = {"client_pid": int(self.client_pid)}
|
|
353
|
+
if self.compact_events:
|
|
354
|
+
params["compact"] = 1
|
|
327
355
|
if self._last_event_sequence is not None:
|
|
328
356
|
params["after_sequence"] = int(self._last_event_sequence)
|
|
329
357
|
return self.url(f"/events?{urlencode(params)}")
|
|
@@ -437,6 +465,8 @@ class AgentClient:
|
|
|
437
465
|
def replay_events(self, before_sequence=None, session_id=None):
|
|
438
466
|
path = "/events/replay"
|
|
439
467
|
params = {}
|
|
468
|
+
if self.compact_events:
|
|
469
|
+
params["compact"] = 1
|
|
440
470
|
if before_sequence is not None:
|
|
441
471
|
params["before_sequence"] = int(before_sequence)
|
|
442
472
|
if session_id:
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import json
|
|
2
2
|
|
|
3
|
+
from modict import modict
|
|
4
|
+
|
|
3
5
|
from ..agent import Agent
|
|
4
6
|
from ..version import __version__
|
|
5
7
|
from ..agent_runtime import InProcessAgentRuntime, ProcessAgentRuntime
|
|
@@ -61,6 +63,22 @@ def create_app(agent: Agent | None = None, start_agent=False, process_agent=Fals
|
|
|
61
63
|
def jsonable(value):
|
|
62
64
|
return json.loads(json.dumps(value, ensure_ascii=False, default=str))
|
|
63
65
|
|
|
66
|
+
def compact_event_payload(payload, *, max_content_chars=2_000):
|
|
67
|
+
payload = modict(jsonable(payload))
|
|
68
|
+
event_type = payload.get("type")
|
|
69
|
+
if event_type in {"tool_call_done_event", "slash_command_done_event"}:
|
|
70
|
+
content_key = "content" if "content" in payload else "result" if "result" in payload else None
|
|
71
|
+
if content_key is not None:
|
|
72
|
+
content = "" if payload.get(content_key) is None else str(payload.get(content_key))
|
|
73
|
+
if len(content) > max_content_chars:
|
|
74
|
+
payload[f"{content_key}_truncated"] = True
|
|
75
|
+
payload[f"{content_key}_full_length"] = len(content)
|
|
76
|
+
payload[content_key] = content[:max_content_chars] + "\n… truncated for compact event stream …"
|
|
77
|
+
return payload
|
|
78
|
+
|
|
79
|
+
def event_payload(payload, compact=False):
|
|
80
|
+
return compact_event_payload(payload) if compact else payload
|
|
81
|
+
|
|
64
82
|
def busy_guard(call):
|
|
65
83
|
try:
|
|
66
84
|
return call()
|
|
@@ -162,7 +180,7 @@ def create_app(agent: Agent | None = None, start_agent=False, process_agent=Fals
|
|
|
162
180
|
return jsonable(service.interrupt(request.reason))
|
|
163
181
|
|
|
164
182
|
@app.get("/events")
|
|
165
|
-
def events(limit: int | None = None, after_sequence: int | None = None, client_pid: int | None = None):
|
|
183
|
+
def events(limit: int | None = None, after_sequence: int | None = None, client_pid: int | None = None, compact: bool = False):
|
|
166
184
|
try:
|
|
167
185
|
subscription = service.subscribe(after_sequence=after_sequence, client_pid=client_pid)
|
|
168
186
|
except RuntimeError as exc:
|
|
@@ -171,6 +189,7 @@ def create_app(agent: Agent | None = None, start_agent=False, process_agent=Fals
|
|
|
171
189
|
def stream():
|
|
172
190
|
count = 0
|
|
173
191
|
for payload in subscription:
|
|
192
|
+
payload = event_payload(payload, compact=compact)
|
|
174
193
|
lines = []
|
|
175
194
|
if payload.get("id"):
|
|
176
195
|
lines.append(f"id: {payload.id}")
|
|
@@ -183,8 +202,12 @@ def create_app(agent: Agent | None = None, start_agent=False, process_agent=Fals
|
|
|
183
202
|
return StreamingResponse(stream(), media_type="text/event-stream")
|
|
184
203
|
|
|
185
204
|
@app.get("/events/replay")
|
|
186
|
-
def replay_events(before_sequence: int | None = None, session_id: str | None = None):
|
|
187
|
-
|
|
205
|
+
def replay_events(before_sequence: int | None = None, session_id: str | None = None, compact: bool = False):
|
|
206
|
+
replay = service.replay_events(before_sequence=before_sequence, session_id=session_id)
|
|
207
|
+
if compact:
|
|
208
|
+
replay = modict(replay)
|
|
209
|
+
replay.events = [compact_event_payload(event) for event in replay.get("events") or []]
|
|
210
|
+
return jsonable(replay)
|
|
188
211
|
|
|
189
212
|
return app
|
|
190
213
|
|
|
@@ -53,17 +53,16 @@ class Tee:
|
|
|
53
53
|
for k in self.queues:
|
|
54
54
|
self.queues[k].put(END)
|
|
55
55
|
|
|
56
|
-
def tee(self,stream, n=2):
|
|
57
|
-
self.queues={k:Queue() for k in range(n)}
|
|
58
|
-
self.thread=Thread(target=self.target, args=(stream,),
|
|
56
|
+
def tee(self, stream, n=2):
|
|
57
|
+
self.queues = {k: Queue() for k in range(n)}
|
|
58
|
+
self.thread = Thread(target=self.target, args=(stream,), daemon=True)
|
|
59
59
|
self.thread.start()
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return tuple(readers.values())
|
|
60
|
+
|
|
61
|
+
def reader(queue):
|
|
62
|
+
while (processed_chunk := queue.get()) is not END:
|
|
63
|
+
yield processed_chunk
|
|
64
|
+
|
|
65
|
+
return tuple(reader(queue) for queue in self.queues.values())
|
|
67
66
|
|
|
68
67
|
class Splitter:
|
|
69
68
|
|
|
@@ -86,17 +85,16 @@ class Splitter:
|
|
|
86
85
|
self.queue_false.put(END)
|
|
87
86
|
|
|
88
87
|
def split(self, stream):
|
|
89
|
-
self.queue_true=Queue()
|
|
90
|
-
self.queue_false=Queue()
|
|
91
|
-
self.thread=Thread(target=self.target, args=(stream,),daemon=True)
|
|
88
|
+
self.queue_true = Queue()
|
|
89
|
+
self.queue_false = Queue()
|
|
90
|
+
self.thread = Thread(target=self.target, args=(stream,), daemon=True)
|
|
92
91
|
self.thread.start()
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def reader_false():
|
|
97
|
-
while (chunk:=self.queue_false.get()) is not END:
|
|
92
|
+
|
|
93
|
+
def reader(queue):
|
|
94
|
+
while (chunk := queue.get()) is not END:
|
|
98
95
|
yield chunk
|
|
99
|
-
|
|
96
|
+
|
|
97
|
+
return reader(self.queue_true), reader(self.queue_false)
|
|
100
98
|
|
|
101
99
|
class Streamer:
|
|
102
100
|
|
|
@@ -159,61 +157,27 @@ class StreamLogger(Streamer):
|
|
|
159
157
|
with open(self.file,'a', encoding='utf-8') as f:
|
|
160
158
|
f.write(output)
|
|
161
159
|
|
|
162
|
-
def fenced(start: Union[str,regex.Pattern,re.Pattern], end: Union[str,regex.Pattern,re.Pattern], flags=0) -> regex.Pattern:
|
|
163
|
-
r"""
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
The end delimiter can include backreferences to captured groups from the opening fence into placeholders in the closing fence.
|
|
168
|
-
|
|
169
|
-
The closing fence pattern may include placeholders in the form of "#1#" for the first captured group,
|
|
170
|
-
"#2#" for the second, or "#name#" for a named capturing group; these will be replaced by the appropriate
|
|
171
|
-
backreference. This mechanism is completely generic and does not enforce any specific delimiter shapes.
|
|
172
|
-
|
|
173
|
-
Example:
|
|
174
|
-
# Using a numeric placeholder:
|
|
175
|
-
pattern = fenced(r"START (\w+)", r"END #1#", flags=regex.DOTALL)
|
|
176
|
-
text = "START content END content"
|
|
177
|
-
match = pattern.match(text)
|
|
178
|
-
if match:
|
|
179
|
-
print(match.group())
|
|
180
|
-
|
|
181
|
-
# Using a named capturing group:
|
|
182
|
-
pattern = fenced(r"BEGIN (?P<tag>\w+)", r"FIN #tag#", flags=regex.DOTALL)
|
|
183
|
-
text = "BEGIN example FIN example"
|
|
184
|
-
match = pattern.match(text)
|
|
185
|
-
if match:
|
|
186
|
-
print(match.group())
|
|
187
|
-
|
|
188
|
-
:param start: Opening delimiter (str or regex.Pattern). May contain capturing groups.
|
|
189
|
-
:param end: Closing delimiter (str or regex.Pattern) which may include placeholders (e.g., "#1#", "#name#").
|
|
190
|
-
:param flags: Regex flags to modify behavior (e.g., regex.DOTALL, regex.MULTILINE).
|
|
191
|
-
:return: A compiled regex pattern.
|
|
160
|
+
def fenced(start: Union[str, regex.Pattern, re.Pattern], end: Union[str, regex.Pattern, re.Pattern], flags=0) -> regex.Pattern:
|
|
161
|
+
r"""Compile a fenced-content pattern with the inner text captured as ``content``.
|
|
162
|
+
|
|
163
|
+
``end`` may reference capture groups from ``start`` with ``#1#`` or
|
|
164
|
+
``#name#`` placeholders, which are converted to regex backreferences.
|
|
192
165
|
"""
|
|
193
|
-
def
|
|
194
|
-
if isinstance(pattern, (regex.Pattern,re.Pattern)):
|
|
195
|
-
return pattern.pattern
|
|
166
|
+
def pattern_source(pattern):
|
|
196
167
|
if isinstance(pattern, str):
|
|
197
168
|
return pattern
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
# Replace placeholders in end pattern with regex backrefs
|
|
204
|
-
def replace_placeholder(match):
|
|
169
|
+
if isinstance(pattern, (regex.Pattern, re.Pattern)):
|
|
170
|
+
return pattern.pattern
|
|
171
|
+
raise ValueError("`start` and `end` must be strings or regex Pattern objects")
|
|
172
|
+
|
|
173
|
+
def placeholder_to_backref(match):
|
|
205
174
|
key = match.group(1)
|
|
206
|
-
if key.isdigit()
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
# Named content group
|
|
213
|
-
inner = rf'(?P<content>(?:(?!{end_regex})[\s\S])*)'
|
|
214
|
-
|
|
215
|
-
full_pattern = rf'{start_regex}{inner}(?:{end_regex}|$)'
|
|
216
|
-
return regex.compile(full_pattern, flags)
|
|
175
|
+
return rf"\{key}" if key.isdigit() else rf"(?P={key})"
|
|
176
|
+
|
|
177
|
+
start_regex = pattern_source(start)
|
|
178
|
+
end_regex = regex.sub(r"#(\w+)#", placeholder_to_backref, pattern_source(end))
|
|
179
|
+
inner = rf"(?P<content>(?:(?!{end_regex})[\s\S])*)"
|
|
180
|
+
return regex.compile(rf"{start_regex}{inner}(?:{end_regex}|$)", flags)
|
|
217
181
|
|
|
218
182
|
def is_compiled_pattern(pattern):
|
|
219
183
|
if isinstance(pattern, (re.Pattern,regex.Pattern)):
|
|
@@ -528,16 +492,19 @@ class MappingStreamSplitter:
|
|
|
528
492
|
def maybe_init_readers(self, data):
|
|
529
493
|
for key in data.keys():
|
|
530
494
|
if key not in self.readers:
|
|
531
|
-
self.queues[key]=Queue()
|
|
532
|
-
|
|
533
|
-
|
|
495
|
+
queue = self.queues[key] = Queue()
|
|
496
|
+
|
|
497
|
+
def reader(queue=queue):
|
|
498
|
+
while (value := queue.get()) is not END:
|
|
534
499
|
if isinstance(value, StreamError):
|
|
535
500
|
raise value.error
|
|
536
501
|
yield value
|
|
537
|
-
|
|
502
|
+
|
|
503
|
+
self.readers[key] = reader()
|
|
538
504
|
|
|
539
505
|
def process(self,stream):
|
|
540
|
-
self.
|
|
506
|
+
self.readers = {}
|
|
507
|
+
self.queues = {}
|
|
541
508
|
self.done=False
|
|
542
509
|
if self.defaults:
|
|
543
510
|
self.maybe_init_readers(self.defaults)
|
|
@@ -6,9 +6,10 @@ from textual.events import Resize
|
|
|
6
6
|
from textual.widgets import Footer, Static, TextArea
|
|
7
7
|
|
|
8
8
|
from .lifecycle import clear_tui, install_tui_cleanup_handlers
|
|
9
|
-
from .
|
|
9
|
+
from .perf import TuiPerfMonitor
|
|
10
|
+
from .stream_debug import TuiStreamDebugLog
|
|
10
11
|
from .style import CHAT_CSS
|
|
11
|
-
from .
|
|
12
|
+
from .widgets import ConversationTimeline
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
def composer_height(text):
|
|
@@ -33,20 +34,13 @@ class AgentTextualApp(App):
|
|
|
33
34
|
super().__init__(**kwargs)
|
|
34
35
|
self.chat = chat
|
|
35
36
|
self.mouse_mode_hint = mouse_mode_hint
|
|
36
|
-
self.transcript_renderer = None
|
|
37
37
|
self._live_delta_flush_scheduled = False
|
|
38
|
+
self.perf = TuiPerfMonitor()
|
|
39
|
+
self.stream_debug = TuiStreamDebugLog()
|
|
38
40
|
|
|
39
41
|
def compose(self) -> ComposeResult:
|
|
40
42
|
with Vertical(id="main"):
|
|
41
|
-
yield
|
|
42
|
-
id="conversation",
|
|
43
|
-
wrap=True,
|
|
44
|
-
highlight=False,
|
|
45
|
-
markup=True,
|
|
46
|
-
on_scroll_top=self.load_older_transcript,
|
|
47
|
-
on_resize=lambda: self.handle_terminal_resize(log_resized=True),
|
|
48
|
-
right_padding=6,
|
|
49
|
-
)
|
|
43
|
+
yield ConversationTimeline(id="conversation")
|
|
50
44
|
yield Static("", id="status")
|
|
51
45
|
yield TextArea(
|
|
52
46
|
"",
|
|
@@ -59,29 +53,26 @@ class AgentTextualApp(App):
|
|
|
59
53
|
|
|
60
54
|
def on_mount(self):
|
|
61
55
|
self.chat.app = self
|
|
62
|
-
self.
|
|
56
|
+
self.chat.client.stream_debug = self.stream_debug
|
|
57
|
+
self.stream_debug.record("tui_mount")
|
|
63
58
|
install_tui_cleanup_handlers()
|
|
64
59
|
self.chat.attach()
|
|
65
60
|
self.chat.client.start_events()
|
|
66
61
|
self.refresh_status()
|
|
67
62
|
self.resize_input()
|
|
68
63
|
self.query_one("#input", TextArea).focus()
|
|
64
|
+
self.set_interval(1.0, self.write_perf_snapshot)
|
|
69
65
|
self.write_info(f"Codex Agent client ready - {self.mouse_mode_hint}")
|
|
70
66
|
|
|
71
67
|
def on_resize(self, event: Resize):
|
|
72
68
|
self.handle_terminal_resize()
|
|
73
69
|
|
|
74
70
|
def handle_terminal_resize(self, *, log_resized=False):
|
|
75
|
-
if getattr(self, "transcript_renderer", None) is None:
|
|
76
|
-
return
|
|
77
|
-
log = self.query_one("#conversation", SelectableRichLog)
|
|
78
|
-
if not log_resized:
|
|
79
|
-
log.invalidate_layout()
|
|
80
|
-
self.render_transcript(keep_scroll=True)
|
|
81
71
|
self.resize_input()
|
|
82
72
|
self.refresh_status()
|
|
83
73
|
|
|
84
74
|
def on_unmount(self):
|
|
75
|
+
self.stream_debug.record("tui_unmount")
|
|
85
76
|
clear_tui(pid=os.getpid())
|
|
86
77
|
|
|
87
78
|
def action_quit(self):
|
|
@@ -113,7 +104,7 @@ class AgentTextualApp(App):
|
|
|
113
104
|
|
|
114
105
|
def action_clear(self):
|
|
115
106
|
self.chat.transcript.clear()
|
|
116
|
-
self.
|
|
107
|
+
self.conversation.clear_messages()
|
|
117
108
|
self.write_info("screen cleared - session history preserved")
|
|
118
109
|
self.refresh_status()
|
|
119
110
|
|
|
@@ -150,77 +141,113 @@ class AgentTextualApp(App):
|
|
|
150
141
|
finally:
|
|
151
142
|
self.call_from_thread(self.refresh_status)
|
|
152
143
|
|
|
144
|
+
@property
|
|
145
|
+
def conversation(self):
|
|
146
|
+
return self.query_one("#conversation", ConversationTimeline)
|
|
147
|
+
|
|
153
148
|
def render_transcript(self, *, keep_scroll=False):
|
|
154
|
-
|
|
149
|
+
# The widget timeline is updated incrementally. This method remains as a
|
|
150
|
+
# compatibility shim for older call sites during the migration.
|
|
151
|
+
return None
|
|
155
152
|
|
|
156
153
|
def begin_transcript_batch(self):
|
|
157
|
-
|
|
154
|
+
return None
|
|
158
155
|
|
|
159
156
|
def end_transcript_batch(self):
|
|
160
|
-
|
|
157
|
+
return None
|
|
161
158
|
|
|
162
159
|
def load_older_transcript(self):
|
|
163
|
-
|
|
160
|
+
return None
|
|
164
161
|
|
|
165
162
|
def write_user(self, prompt):
|
|
166
163
|
self.chat.transcript.append_user(prompt, self.chat.username())
|
|
167
|
-
self.
|
|
164
|
+
self.conversation.add_user(prompt, title=self.chat.username())
|
|
168
165
|
|
|
169
166
|
def ensure_agent_role(self, name=None):
|
|
170
|
-
|
|
171
|
-
|
|
167
|
+
self.chat.transcript.ensure_agent(name or self.chat.agent_name())
|
|
168
|
+
return self.conversation.ensure_active_assistant(title=name or self.chat.agent_name())
|
|
172
169
|
|
|
173
170
|
def write_agent_start(self, name):
|
|
174
171
|
self.chat.begin_turn_rendering()
|
|
175
172
|
|
|
176
173
|
def start_agent_step(self):
|
|
177
174
|
self.chat.transcript.begin_step()
|
|
178
|
-
self.render_transcript()
|
|
179
175
|
|
|
180
176
|
def write_agent_delta(self, delta):
|
|
177
|
+
self.stream_debug.record("ui_render_delta_begin", delta_len=len(str(delta or "")))
|
|
178
|
+
self.perf.count("write_agent_delta")
|
|
179
|
+
self.perf.count("write_agent_delta_chars", len(str(delta or "")))
|
|
180
|
+
with self.perf.timed("write_agent_delta"):
|
|
181
|
+
self.ensure_agent_role()
|
|
182
|
+
self.chat.transcript.apply_agent_delta(delta)
|
|
183
|
+
self.chat.transcript.step_had_content = True
|
|
184
|
+
self.conversation.append_assistant_delta(delta, title=self.chat.agent_name())
|
|
185
|
+
self.stream_debug.record("ui_render_delta_done", delta_len=len(str(delta or "")), live_chars=len(self.conversation._active_assistant_text))
|
|
186
|
+
|
|
187
|
+
def write_agent_message(self, content):
|
|
181
188
|
self.ensure_agent_role()
|
|
182
|
-
|
|
183
|
-
self.chat.transcript.
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
self.render_transcript()
|
|
189
|
+
self.chat.transcript.agent_buffer = str(content or "")
|
|
190
|
+
if self.chat.transcript.entries and self.chat.transcript.entries[-1].kind == "agent":
|
|
191
|
+
self.chat.transcript.entries[-1].text = self.chat.transcript.agent_buffer
|
|
192
|
+
self.conversation.add_assistant_message(content, title=self.chat.agent_name())
|
|
187
193
|
|
|
188
194
|
def schedule_agent_delta_flush(self):
|
|
195
|
+
self.perf.count("schedule_agent_delta_flush")
|
|
189
196
|
if self._live_delta_flush_scheduled:
|
|
190
197
|
return
|
|
191
198
|
self._live_delta_flush_scheduled = True
|
|
192
199
|
self.set_timer(self.chat.live_delta.interval, self.flush_agent_delta)
|
|
193
200
|
|
|
194
201
|
def flush_agent_delta(self):
|
|
195
|
-
self.
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
self.
|
|
202
|
+
self.perf.count("flush_agent_delta")
|
|
203
|
+
with self.perf.timed("flush_agent_delta"):
|
|
204
|
+
self._live_delta_flush_scheduled = False
|
|
205
|
+
delta = self.chat.live_delta.pop()
|
|
206
|
+
if delta:
|
|
207
|
+
self.write_agent_delta(delta)
|
|
199
208
|
|
|
200
209
|
def write_tool(self, name, call_id=None, status=None):
|
|
201
210
|
self.flush_agent_delta()
|
|
202
|
-
self.
|
|
211
|
+
self.conversation.commit_active_assistant_preview()
|
|
203
212
|
self.chat.transcript.append_tool(name, call_id=call_id, status=status)
|
|
204
|
-
self.
|
|
213
|
+
self.conversation.add_tool(name, call_id=call_id, status=status)
|
|
205
214
|
|
|
206
215
|
def write_slash_command(self, name, args="", status=None):
|
|
207
216
|
self.flush_agent_delta()
|
|
208
|
-
self.
|
|
217
|
+
self.conversation.commit_active_assistant_preview()
|
|
209
218
|
self.chat.transcript.append_slash_command(name, args=args, status=status)
|
|
210
|
-
self.
|
|
219
|
+
self.conversation.add_slash_command(name, args=args, status=status)
|
|
211
220
|
|
|
212
221
|
def end_agent_step(self):
|
|
213
222
|
self.flush_agent_delta()
|
|
223
|
+
self.conversation.commit_active_assistant_preview()
|
|
214
224
|
self.chat.transcript.end_step()
|
|
215
|
-
self.
|
|
225
|
+
self.conversation.add_step_separator()
|
|
216
226
|
|
|
217
227
|
def write_info(self, text):
|
|
218
228
|
self.chat.transcript.append_info(text)
|
|
219
|
-
self.
|
|
229
|
+
self.conversation.add_info(text)
|
|
220
230
|
|
|
221
231
|
def write_error(self, text):
|
|
222
232
|
self.chat.transcript.append_error(text)
|
|
223
|
-
self.
|
|
233
|
+
self.conversation.add_error(text)
|
|
224
234
|
|
|
225
235
|
def refresh_status(self):
|
|
226
|
-
self.
|
|
236
|
+
self.perf.count("refresh_status")
|
|
237
|
+
with self.perf.timed("refresh_status"):
|
|
238
|
+
self.query_one("#status", Static).update(self.chat.status_renderable())
|
|
239
|
+
|
|
240
|
+
def write_perf_snapshot(self):
|
|
241
|
+
client = self.chat.client
|
|
242
|
+
queue_obj = getattr(client, "_local_event_queue", None)
|
|
243
|
+
extra = {
|
|
244
|
+
"entries": len(self.chat.transcript.entries),
|
|
245
|
+
"live_buffer_chars": len(self.conversation._active_assistant_text),
|
|
246
|
+
"pending_live_delta_chars": len(self.chat.live_delta.text),
|
|
247
|
+
"last_event_sequence": getattr(client, "_last_event_sequence", None),
|
|
248
|
+
"event_queue_size": queue_obj.qsize() if queue_obj is not None else None,
|
|
249
|
+
"event_stream_connected": getattr(client, "_event_stream_connected", None),
|
|
250
|
+
"live_delta_flush_scheduled": self._live_delta_flush_scheduled,
|
|
251
|
+
}
|
|
252
|
+
self.perf.write_snapshot(extra=extra)
|
|
253
|
+
self.stream_debug.record("tui_heartbeat", **extra)
|
|
@@ -47,7 +47,7 @@ class Chat:
|
|
|
47
47
|
"""Textual-based chat UI client for a codex-agent server."""
|
|
48
48
|
|
|
49
49
|
def __init__(self, client=None, base_url="http://127.0.0.1:8765"):
|
|
50
|
-
self.client = client or AgentClient(base_url=base_url)
|
|
50
|
+
self.client = client or AgentClient(base_url=base_url, compact_events=True)
|
|
51
51
|
self.app = None
|
|
52
52
|
self.view = TuiAppBridge(self)
|
|
53
53
|
self.deduper = DisplayDeduper()
|
|
@@ -233,6 +233,9 @@ class Chat:
|
|
|
233
233
|
message = event.get("message") or {}
|
|
234
234
|
if message.get("type") == "user_message":
|
|
235
235
|
self.view.write_user(str(message.get("content") or ""))
|
|
236
|
+
elif message.get("type") == "assistant_message":
|
|
237
|
+
self._flush_live_delta()
|
|
238
|
+
self.view.write_agent_message(str(message.get("content") or ""))
|
|
236
239
|
self.view.refresh_status()
|
|
237
240
|
|
|
238
241
|
def _on_assistant_turn_start(self, event):
|