codex-agent-framework 0.1.54__tar.gz → 0.1.55__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.
Files changed (152) hide show
  1. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/CHANGELOG.md +8 -0
  2. {codex_agent_framework-0.1.54/codex_agent_framework.egg-info → codex_agent_framework-0.1.55}/PKG-INFO +3 -1
  3. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/README.md +2 -0
  4. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/client.py +37 -5
  5. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/tui/state.py +1 -4
  6. codex_agent_framework-0.1.55/codex_agent/version.py +1 -0
  7. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55/codex_agent_framework.egg-info}/PKG-INFO +3 -1
  8. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/pyproject.toml +1 -1
  9. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_chat.py +23 -0
  10. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_client.py +77 -4
  11. codex_agent_framework-0.1.54/codex_agent/version.py +0 -1
  12. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/LICENSE +0 -0
  13. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/MANIFEST.in +0 -0
  14. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/__init__.py +0 -0
  15. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/__main__.py +0 -0
  16. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/agent.py +0 -0
  17. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/agent_runtime.py +0 -0
  18. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/ai.py +0 -0
  19. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/browser.py +0 -0
  20. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_commands.py +0 -0
  21. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/__init__.py +0 -0
  22. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/bash/__init__.py +0 -0
  23. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/bash/requirements.txt +0 -0
  24. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/browser/__init__.py +0 -0
  25. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/browser/requirements.txt +0 -0
  26. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/content/__init__.py +0 -0
  27. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/content/requirements.txt +0 -0
  28. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/content/system.py +0 -0
  29. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/content/vision.py +0 -0
  30. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/context/__init__.py +0 -0
  31. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/desktop/__init__.py +0 -0
  32. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/desktop/requirements.txt +0 -0
  33. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/environment/__init__.py +0 -0
  34. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/environment/requirements.txt +0 -0
  35. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/files/__init__.py +0 -0
  36. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/files/requirements.txt +0 -0
  37. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/files/tools.py +0 -0
  38. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/image_generation/__init__.py +0 -0
  39. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/image_generation/image_generation_system_prompt.txt +0 -0
  40. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/image_generation/requirements.txt +0 -0
  41. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/interface/__init__.py +0 -0
  42. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/interface/requirements.txt +0 -0
  43. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/memory/__init__.py +0 -0
  44. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/memory/requirements.txt +0 -0
  45. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/planner/__init__.py +0 -0
  46. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/planner/requirements.txt +0 -0
  47. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/python/__init__.py +0 -0
  48. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/python/requirements.txt +0 -0
  49. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/realtime/__init__.py +0 -0
  50. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/realtime/audio.py +0 -0
  51. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/realtime/bridge.py +0 -0
  52. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/realtime/manager.py +0 -0
  53. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/realtime/requirements.txt +0 -0
  54. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/realtime/session.py +0 -0
  55. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/scheduler/__init__.py +0 -0
  56. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/scheduler/requirements.txt +0 -0
  57. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/subagents/__init__.py +0 -0
  58. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/subagents/profiles/explorer.json +0 -0
  59. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/subagents/requirements.txt +0 -0
  60. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/tts/__init__.py +0 -0
  61. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/tts/requirements.txt +0 -0
  62. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/builtin_plugins/web_search/__init__.py +0 -0
  63. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/cli/__init__.py +0 -0
  64. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/cli/headless.py +0 -0
  65. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/cli/main.py +0 -0
  66. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/cli/output.py +0 -0
  67. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/cli/root.py +0 -0
  68. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/cli/runner.py +0 -0
  69. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/command.py +0 -0
  70. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/config.py +0 -0
  71. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/context.py +0 -0
  72. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/desktop.py +0 -0
  73. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/event.py +0 -0
  74. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/get_text/__init__.py +0 -0
  75. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/get_text/default_gitignore +0 -0
  76. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/get_text/get_text.py +0 -0
  77. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/get_text/simpler_get_text.py +0 -0
  78. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/hooks.py +0 -0
  79. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/image.py +0 -0
  80. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/latex.py +0 -0
  81. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/mainloop.py +0 -0
  82. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/message.py +0 -0
  83. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/plugin.py +0 -0
  84. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/prompts/system_prompt.txt +0 -0
  85. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/provider.py +0 -0
  86. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/runtime.py +0 -0
  87. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/scheduler.py +0 -0
  88. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/scripts/install-system-dependencies.sh +0 -0
  89. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/server/__init__.py +0 -0
  90. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/server/app.py +0 -0
  91. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/server/core.py +0 -0
  92. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/service.py +0 -0
  93. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/sessions.py +0 -0
  94. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/status.py +0 -0
  95. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/stream.py +0 -0
  96. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/stream_utils.py +0 -0
  97. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/tool.py +0 -0
  98. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/tray.py +0 -0
  99. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/tui/__init__.py +0 -0
  100. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/tui/app.py +0 -0
  101. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/tui/chat.py +0 -0
  102. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/tui/lifecycle.py +0 -0
  103. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/tui/log.py +0 -0
  104. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/tui/status_bar.py +0 -0
  105. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/tui/style.py +0 -0
  106. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/tui/transcript.py +0 -0
  107. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/utils.py +0 -0
  108. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent/worker.py +0 -0
  109. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent_framework.egg-info/SOURCES.txt +0 -0
  110. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent_framework.egg-info/dependency_links.txt +0 -0
  111. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent_framework.egg-info/entry_points.txt +0 -0
  112. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent_framework.egg-info/requires.txt +0 -0
  113. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/codex_agent_framework.egg-info/top_level.txt +0 -0
  114. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/dependencies.txt +0 -0
  115. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/setup.cfg +0 -0
  116. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_agent.py +0 -0
  117. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_agent_audio_hooks.py +0 -0
  118. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_agent_commands.py +0 -0
  119. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_agent_context.py +0 -0
  120. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_agent_desktop.py +0 -0
  121. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_agent_environment.py +0 -0
  122. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_agent_image_generation.py +0 -0
  123. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_agent_observe.py +0 -0
  124. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_agent_plugins.py +0 -0
  125. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_agent_providers_config.py +0 -0
  126. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_agent_realtime.py +0 -0
  127. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_agent_response.py +0 -0
  128. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_agent_runtime.py +0 -0
  129. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_agent_sessions.py +0 -0
  130. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_agent_subagents.py +0 -0
  131. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_agent_tools.py +0 -0
  132. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_agent_turns.py +0 -0
  133. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_ai.py +0 -0
  134. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_browser.py +0 -0
  135. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_builtin_config.py +0 -0
  136. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_cli.py +0 -0
  137. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_cli_headless.py +0 -0
  138. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_cli_root.py +0 -0
  139. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_events.py +0 -0
  140. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_get_text_browser.py +0 -0
  141. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_hooks.py +0 -0
  142. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_image_message.py +0 -0
  143. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_local_desktop.py +0 -0
  144. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_memory.py +0 -0
  145. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_messages.py +0 -0
  146. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_planner.py +0 -0
  147. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_scheduler.py +0 -0
  148. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_server.py +0 -0
  149. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_service.py +0 -0
  150. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_tui.py +0 -0
  151. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_utils.py +0 -0
  152. {codex_agent_framework-0.1.54 → codex_agent_framework-0.1.55}/tests/test_worker.py +0 -0
@@ -4,6 +4,14 @@ 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.55] - 2026-05-17
8
+ ### Fixed
9
+ - 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.
10
+ - Remove the unsafe TUI bridge fallback that could execute UI callbacks off the Textual thread after `call_from_thread(...)` failures.
11
+
12
+ ### Tests
13
+ - Add regression coverage for slow local handlers and TUI bridge thread-safety, then validate with targeted client/chat tests (88 passed).
14
+
7
15
  ## [0.1.54] - 2026-05-17
8
16
  ### Changed
9
17
  - Add a generic namespaced `/config` command surface so agent and plugin configs can be read, updated, and saved through a uniform dot-notation interface such as `/config agent.input_token_limit 200000`, `/config tts.voice nova`, `/config agent save`, and `/config save`.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-agent-framework
3
- Version: 0.1.54
3
+ Version: 0.1.55
4
4
  Summary: A lightweight event-driven Codex agent runtime.
5
5
  Author: Baptiste
6
6
  License-Expression: MIT
@@ -129,6 +129,8 @@ turn = stream.turn
129
129
  print("\ncompleted:", turn.completed)
130
130
  ```
131
131
 
132
+ The built-in TUI uses the same streamed event model and is designed to stay responsive during heavier tool activity by decoupling event intake from UI rendering.
133
+
132
134
  Add a small tool:
133
135
 
134
136
  ```python
@@ -80,6 +80,8 @@ turn = stream.turn
80
80
  print("\ncompleted:", turn.completed)
81
81
  ```
82
82
 
83
+ The built-in TUI uses the same streamed event model and is designed to stay responsive during heavier tool activity by decoupling event intake from UI rendering.
84
+
83
85
  Add a small tool:
84
86
 
85
87
  ```python
@@ -1,5 +1,6 @@
1
1
  import json
2
2
  import os
3
+ import queue
3
4
  import threading
4
5
  import time
5
6
  from urllib.parse import urlencode
@@ -22,7 +23,9 @@ class AgentClient:
22
23
  self._event_handlers = {}
23
24
  self._event_thread = None
24
25
  self._health_thread = None
26
+ self._dispatch_thread = None
25
27
  self._event_stop = threading.Event()
28
+ self._local_event_queue = queue.Queue()
26
29
  self._server_connected = False
27
30
  self._event_stream_connected = False
28
31
  self._event_stream_response = None
@@ -77,10 +80,35 @@ class AgentClient:
77
80
  self.emit_local({"type": "client_event_error", "error": str(exc), "event": event_type})
78
81
  return event
79
82
 
83
+ def _start_dispatch_thread(self):
84
+ if self._dispatch_thread and self._dispatch_thread.is_alive():
85
+ return self
86
+ self._dispatch_thread = threading.Thread(target=self._dispatch_loop, name="agent-client-dispatch", daemon=True)
87
+ self._dispatch_thread.start()
88
+ return self
89
+
90
+ def _enqueue_local(self, event):
91
+ self._start_dispatch_thread()
92
+ self._local_event_queue.put(modict(event))
93
+ return event
94
+
95
+ def _dispatch_loop(self):
96
+ while True:
97
+ try:
98
+ event = self._local_event_queue.get(timeout=0.1)
99
+ except queue.Empty:
100
+ if self._event_stop.is_set():
101
+ break
102
+ continue
103
+ if event is None:
104
+ break
105
+ self.emit_local(event)
106
+
80
107
  def start_events(self):
81
108
  if self._event_thread and self._event_thread.is_alive():
82
109
  return self
83
110
  self._event_stop.clear()
111
+ self._start_dispatch_thread()
84
112
  self._event_thread = threading.Thread(target=self._event_loop, name="agent-client-events", daemon=True)
85
113
  self._health_thread = threading.Thread(target=self._health_loop, name="agent-client-health", daemon=True)
86
114
  self._event_thread.start()
@@ -89,10 +117,13 @@ class AgentClient:
89
117
 
90
118
  def stop_events(self, timeout=0.2):
91
119
  self._event_stop.set()
120
+ self._local_event_queue.put(None)
92
121
  if self._event_thread:
93
122
  self._event_thread.join(timeout)
94
123
  if self._health_thread:
95
124
  self._health_thread.join(timeout)
125
+ if self._dispatch_thread:
126
+ self._dispatch_thread.join(timeout)
96
127
  return self
97
128
 
98
129
  def _set_server_connected(self):
@@ -100,7 +131,7 @@ class AgentClient:
100
131
  if self._server_connected:
101
132
  return False
102
133
  self._server_connected = True
103
- self.emit_local({"type": "server_connected"})
134
+ self._enqueue_local({"type": "server_connected"})
104
135
  return True
105
136
 
106
137
  def _set_server_offline(self, error=""):
@@ -108,7 +139,7 @@ class AgentClient:
108
139
  if not self._server_connected:
109
140
  return False
110
141
  self._server_connected = False
111
- self.emit_local({"type": "server_offline", "error": str(error or "")})
142
+ self._enqueue_local({"type": "server_offline", "error": str(error or "")})
112
143
  return True
113
144
 
114
145
  def _set_event_stream_connected(self, connected):
@@ -180,6 +211,7 @@ class AgentClient:
180
211
  self._event_stop.wait(self.health_interval)
181
212
 
182
213
  def _event_loop(self):
214
+ self._start_dispatch_thread()
183
215
  while not self._event_stop.is_set():
184
216
  try:
185
217
  with requests.get(
@@ -200,7 +232,7 @@ class AgentClient:
200
232
  break
201
233
  self._record_event_stream_activity()
202
234
  self.remember_event_sequence(event)
203
- self.emit_local(event)
235
+ self._enqueue_local(event)
204
236
  finally:
205
237
  self._clear_event_stream_response(response)
206
238
  if stream_ended and not self._event_stop.is_set():
@@ -214,7 +246,7 @@ class AgentClient:
214
246
  reconnecting = self._consume_event_reconnect_requested()
215
247
  if not (reconnecting or (self._server_connected and self.is_event_idle_timeout(exc))):
216
248
  self._set_server_offline(str(exc))
217
- self.emit_local(self.event_error_payload(exc))
249
+ self._enqueue_local(self.event_error_payload(exc))
218
250
  self._event_stop.wait(0.1)
219
251
 
220
252
  def event_error_payload(self, exc):
@@ -368,7 +400,7 @@ class AgentClient:
368
400
  return self.post("/interrupt", {"reason": reason})
369
401
  except Exception as exc:
370
402
  payload = modict(interrupted=False, interrupt_requested=False, error=str(exc))
371
- self.emit_local({
403
+ self._enqueue_local({
372
404
  "type": "client_event_error",
373
405
  "error": str(exc),
374
406
  "source": "interrupt",
@@ -60,10 +60,7 @@ class TuiAppBridge:
60
60
  if self.app is None:
61
61
  return None
62
62
  callback = getattr(self.app, method)
63
- try:
64
- return self.app.call_from_thread(callback, *args)
65
- except RuntimeError:
66
- return callback(*args)
63
+ return self.app.call_from_thread(callback, *args)
67
64
 
68
65
  def refresh_status(self):
69
66
  return self.call("refresh_status", skip_during_replay=True)
@@ -0,0 +1 @@
1
+ __version__ = "0.1.55"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-agent-framework
3
- Version: 0.1.54
3
+ Version: 0.1.55
4
4
  Summary: A lightweight event-driven Codex agent runtime.
5
5
  Author: Baptiste
6
6
  License-Expression: MIT
@@ -129,6 +129,8 @@ turn = stream.turn
129
129
  print("\ncompleted:", turn.completed)
130
130
  ```
131
131
 
132
+ The built-in TUI uses the same streamed event model and is designed to stay responsive during heavier tool activity by decoupling event intake from UI rendering.
133
+
132
134
  Add a small tool:
133
135
 
134
136
  ```python
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codex-agent-framework"
7
- version = "0.1.54"
7
+ version = "0.1.55"
8
8
  description = "A lightweight event-driven Codex agent runtime."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -1204,6 +1204,29 @@ def test_chat_live_deltas_render_before_tool_start(monkeypatch):
1204
1204
  assert calls.index(("write_agent_delta", ("I will inspect.",))) < calls.index(("write_tool", ("bash", "call_1", "running")))
1205
1205
 
1206
1206
 
1207
+ def test_tui_app_bridge_does_not_fallback_to_off_thread_callback_execution():
1208
+ chat = Chat()
1209
+ calls = []
1210
+
1211
+ class App:
1212
+ def call_from_thread(self, callback, *args):
1213
+ raise RuntimeError("app not running")
1214
+
1215
+ def write_agent_delta(self, delta):
1216
+ calls.append(("delta", delta))
1217
+
1218
+ chat.app = App()
1219
+
1220
+ try:
1221
+ chat.view.write_agent_delta("hello")
1222
+ except RuntimeError as exc:
1223
+ assert "app not running" in str(exc)
1224
+ else:
1225
+ raise AssertionError("expected RuntimeError")
1226
+
1227
+ assert calls == []
1228
+
1229
+
1207
1230
  def test_chat_flushes_replay_deltas_before_live_deltas(monkeypatch):
1208
1231
  chat = Chat()
1209
1232
  calls = []
@@ -1,4 +1,6 @@
1
1
  import json
2
+ import threading
3
+ import time
2
4
 
3
5
  import requests
4
6
 
@@ -47,6 +49,72 @@ def test_agent_client_keeps_dispatching_when_handler_fails():
47
49
  assert seen == ["boom"]
48
50
 
49
51
 
52
+ def test_agent_client_local_dispatch_queue_decouples_sse_from_slow_handler(monkeypatch):
53
+ client = AgentClient(timeout=0.01)
54
+ started = threading.Event()
55
+ release = threading.Event()
56
+ handled = []
57
+ request_count = {"count": 0}
58
+
59
+ def slow_handler(event):
60
+ handled.append(("start", event.type))
61
+ started.set()
62
+ release.wait(timeout=1.0)
63
+ handled.append(("done", event.type))
64
+
65
+ client.on("assistant_turn_start_event", slow_handler)
66
+ client.on("tool_call_start_event", lambda event: (handled.append(("fast", event.type)), client._event_stop.set()))
67
+
68
+ class Response:
69
+ content = b""
70
+
71
+ def __init__(self, lines):
72
+ self.lines = lines
73
+
74
+ def __enter__(self):
75
+ return self
76
+
77
+ def __exit__(self, *args):
78
+ return False
79
+
80
+ def raise_for_status(self):
81
+ pass
82
+
83
+ def iter_lines(self, chunk_size=None, decode_unicode=True):
84
+ for line in self.lines:
85
+ yield line
86
+
87
+ def fake_get(*args, **kwargs):
88
+ request_count["count"] += 1
89
+ if request_count["count"] == 1:
90
+ return Response([
91
+ 'event: assistant_turn_start_event',
92
+ 'data: {"type": "assistant_turn_start_event", "sequence": 1}',
93
+ '',
94
+ 'event: tool_call_start_event',
95
+ 'data: {"type": "tool_call_start_event", "sequence": 2}',
96
+ '',
97
+ ])
98
+ return Response([])
99
+
100
+ monkeypatch.setattr("codex_agent.client.requests.get", fake_get)
101
+ monkeypatch.setattr(client._event_stop, "wait", lambda delay: None)
102
+
103
+ thread = threading.Thread(target=client._event_loop, daemon=True)
104
+ thread.start()
105
+
106
+ assert started.wait(timeout=1.0) is True
107
+ assert client._last_event_sequence == 2
108
+
109
+ release.set()
110
+ thread.join(timeout=1.0)
111
+ client.stop_events(timeout=0.2)
112
+
113
+ assert ("start", "assistant_turn_start_event") in handled
114
+ assert ("done", "assistant_turn_start_event") in handled
115
+ assert ("fast", "tool_call_start_event") in handled
116
+
117
+
50
118
  def test_agent_client_parses_bad_sse_json_as_client_error():
51
119
  client = AgentClient()
52
120
  lines = [
@@ -113,7 +181,8 @@ def test_agent_client_submit_prompt_posts_prompt(monkeypatch):
113
181
  def test_agent_client_interrupt_is_best_effort_on_http_error(monkeypatch):
114
182
  client = AgentClient(base_url="http://agent.local")
115
183
  seen = []
116
- client.on("client_event_error", lambda event: seen.append(event))
184
+ received = threading.Event()
185
+ client.on("client_event_error", lambda event: (seen.append(event), received.set()))
117
186
 
118
187
  def fake_request(method, url, **kwargs):
119
188
  raise requests.ConnectionError("server disappeared")
@@ -125,7 +194,9 @@ def test_agent_client_interrupt_is_best_effort_on_http_error(monkeypatch):
125
194
  assert result.interrupted is False
126
195
  assert result.interrupt_requested is False
127
196
  assert "server disappeared" in result.error
197
+ assert received.wait(timeout=1.0) is True
128
198
  assert seen[-1].source == "interrupt"
199
+ client.stop_events(timeout=0.2)
129
200
 
130
201
 
131
202
  def test_agent_client_fetches_replay_events_with_cursor_and_session(monkeypatch):
@@ -195,8 +266,9 @@ def test_agent_client_emits_offline_then_connected_on_reconnect(monkeypatch):
195
266
  monkeypatch.setattr(client._event_stop, "wait", lambda delay: None)
196
267
 
197
268
  client._event_loop()
269
+ client.stop_events(timeout=0.2)
198
270
 
199
- assert events == ["server_connected", "server_ping", "server_offline", "server_connected", "server_ping"]
271
+ assert events[:5] == ["server_connected", "server_ping", "server_offline", "server_connected", "server_ping"]
200
272
 
201
273
 
202
274
  def test_agent_client_does_not_emit_server_connected_for_failed_connection(monkeypatch):
@@ -347,12 +419,13 @@ def test_agent_client_reconnects_after_idle_read_timeout_without_offline_event(m
347
419
  monkeypatch.setattr(client._event_stop, "wait", lambda delay: False)
348
420
 
349
421
  client._event_loop()
422
+ client.stop_events(timeout=0.2)
350
423
 
351
- assert urls == [
424
+ assert urls[:2] == [
352
425
  "http://agent.local/events?client_pid=1234",
353
426
  "http://agent.local/events?client_pid=1234&after_sequence=2",
354
427
  ]
355
- assert events == ["server_connected", "tool_call_start_event", "tool_call_done_event"]
428
+ assert events[:3] == ["server_connected", "tool_call_start_event", "tool_call_done_event"]
356
429
 
357
430
 
358
431
  def test_agent_client_health_loop_reconnects_stale_event_stream_without_offline(monkeypatch):
@@ -1 +0,0 @@
1
- __version__ = "0.1.54"