codex-agent-framework 0.1.56__tar.gz → 0.1.58__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 (155) hide show
  1. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/CHANGELOG.md +19 -0
  2. {codex_agent_framework-0.1.56/codex_agent_framework.egg-info → codex_agent_framework-0.1.58}/PKG-INFO +1 -1
  3. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/server/core.py +7 -4
  4. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/stream_utils.py +42 -75
  5. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/utils.py +30 -57
  6. codex_agent_framework-0.1.58/codex_agent/version.py +1 -0
  7. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58/codex_agent_framework.egg-info}/PKG-INFO +1 -1
  8. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/pyproject.toml +1 -1
  9. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_server.py +21 -9
  10. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_utils.py +87 -1
  11. codex_agent_framework-0.1.56/codex_agent/version.py +0 -1
  12. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/LICENSE +0 -0
  13. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/MANIFEST.in +0 -0
  14. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/README.md +0 -0
  15. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/__init__.py +0 -0
  16. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/__main__.py +0 -0
  17. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/agent.py +0 -0
  18. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/agent_runtime.py +0 -0
  19. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/ai.py +0 -0
  20. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/browser.py +0 -0
  21. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_commands.py +0 -0
  22. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/__init__.py +0 -0
  23. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/bash/__init__.py +0 -0
  24. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/bash/requirements.txt +0 -0
  25. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/browser/__init__.py +0 -0
  26. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/browser/requirements.txt +0 -0
  27. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/content/__init__.py +0 -0
  28. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/content/requirements.txt +0 -0
  29. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/content/system.py +0 -0
  30. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/content/vision.py +0 -0
  31. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/context/__init__.py +0 -0
  32. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/desktop/__init__.py +0 -0
  33. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/desktop/requirements.txt +0 -0
  34. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/environment/__init__.py +0 -0
  35. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/environment/requirements.txt +0 -0
  36. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/files/__init__.py +0 -0
  37. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/files/requirements.txt +0 -0
  38. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/files/tools.py +0 -0
  39. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/image_generation/__init__.py +0 -0
  40. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/image_generation/image_generation_system_prompt.txt +0 -0
  41. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/image_generation/requirements.txt +0 -0
  42. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/interface/__init__.py +0 -0
  43. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/interface/requirements.txt +0 -0
  44. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/memory/__init__.py +0 -0
  45. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/memory/requirements.txt +0 -0
  46. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/planner/__init__.py +0 -0
  47. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/planner/requirements.txt +0 -0
  48. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/python/__init__.py +0 -0
  49. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/python/requirements.txt +0 -0
  50. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/realtime/__init__.py +0 -0
  51. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/realtime/audio.py +0 -0
  52. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/realtime/bridge.py +0 -0
  53. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/realtime/manager.py +0 -0
  54. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/realtime/requirements.txt +0 -0
  55. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/realtime/session.py +0 -0
  56. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/scheduler/__init__.py +0 -0
  57. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/scheduler/requirements.txt +0 -0
  58. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/subagents/__init__.py +0 -0
  59. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/subagents/profiles/explorer.json +0 -0
  60. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/subagents/requirements.txt +0 -0
  61. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/tts/__init__.py +0 -0
  62. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/tts/requirements.txt +0 -0
  63. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/builtin_plugins/web_search/__init__.py +0 -0
  64. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/cli/__init__.py +0 -0
  65. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/cli/headless.py +0 -0
  66. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/cli/main.py +0 -0
  67. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/cli/output.py +0 -0
  68. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/cli/root.py +0 -0
  69. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/cli/runner.py +0 -0
  70. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/client.py +0 -0
  71. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/command.py +0 -0
  72. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/config.py +0 -0
  73. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/context.py +0 -0
  74. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/desktop.py +0 -0
  75. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/event.py +0 -0
  76. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/get_text/__init__.py +0 -0
  77. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/get_text/default_gitignore +0 -0
  78. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/get_text/get_text.py +0 -0
  79. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/get_text/simpler_get_text.py +0 -0
  80. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/hooks.py +0 -0
  81. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/image.py +0 -0
  82. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/latex.py +0 -0
  83. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/mainloop.py +0 -0
  84. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/message.py +0 -0
  85. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/plugin.py +0 -0
  86. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/prompts/system_prompt.txt +0 -0
  87. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/provider.py +0 -0
  88. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/runtime.py +0 -0
  89. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/scheduler.py +0 -0
  90. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/scripts/install-system-dependencies.sh +0 -0
  91. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/server/__init__.py +0 -0
  92. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/server/app.py +0 -0
  93. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/service.py +0 -0
  94. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/sessions.py +0 -0
  95. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/status.py +0 -0
  96. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/stream.py +0 -0
  97. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/tool.py +0 -0
  98. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/tray.py +0 -0
  99. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/tui/__init__.py +0 -0
  100. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/tui/app.py +0 -0
  101. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/tui/chat.py +0 -0
  102. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/tui/lifecycle.py +0 -0
  103. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/tui/log.py +0 -0
  104. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/tui/perf.py +0 -0
  105. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/tui/state.py +0 -0
  106. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/tui/status_bar.py +0 -0
  107. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/tui/stream_debug.py +0 -0
  108. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/tui/style.py +0 -0
  109. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/tui/transcript.py +0 -0
  110. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/tui/widgets.py +0 -0
  111. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent/worker.py +0 -0
  112. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent_framework.egg-info/SOURCES.txt +0 -0
  113. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent_framework.egg-info/dependency_links.txt +0 -0
  114. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent_framework.egg-info/entry_points.txt +0 -0
  115. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent_framework.egg-info/requires.txt +0 -0
  116. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/codex_agent_framework.egg-info/top_level.txt +0 -0
  117. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/dependencies.txt +0 -0
  118. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/setup.cfg +0 -0
  119. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_agent.py +0 -0
  120. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_agent_audio_hooks.py +0 -0
  121. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_agent_commands.py +0 -0
  122. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_agent_context.py +0 -0
  123. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_agent_desktop.py +0 -0
  124. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_agent_environment.py +0 -0
  125. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_agent_image_generation.py +0 -0
  126. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_agent_observe.py +0 -0
  127. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_agent_plugins.py +0 -0
  128. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_agent_providers_config.py +0 -0
  129. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_agent_realtime.py +0 -0
  130. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_agent_response.py +0 -0
  131. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_agent_runtime.py +0 -0
  132. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_agent_sessions.py +0 -0
  133. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_agent_subagents.py +0 -0
  134. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_agent_tools.py +0 -0
  135. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_agent_turns.py +0 -0
  136. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_ai.py +0 -0
  137. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_browser.py +0 -0
  138. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_builtin_config.py +0 -0
  139. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_chat.py +0 -0
  140. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_cli.py +0 -0
  141. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_cli_headless.py +0 -0
  142. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_cli_root.py +0 -0
  143. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_client.py +0 -0
  144. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_events.py +0 -0
  145. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_get_text_browser.py +0 -0
  146. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_hooks.py +0 -0
  147. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_image_message.py +0 -0
  148. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_local_desktop.py +0 -0
  149. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_memory.py +0 -0
  150. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_messages.py +0 -0
  151. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_planner.py +0 -0
  152. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_scheduler.py +0 -0
  153. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_service.py +0 -0
  154. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_tui.py +0 -0
  155. {codex_agent_framework-0.1.56 → codex_agent_framework-0.1.58}/tests/test_worker.py +0 -0
@@ -4,6 +4,25 @@ 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.58] - 2026-05-17
8
+ ### Fixed
9
+ - Keep active SSE event subscribers attached even when the FastAPI request handler thread that opened the `StreamingResponse` has ended, preventing TUI streams from degrading into ping-only connections that miss real assistant/tool events.
10
+ - Stop an SSE stream promptly if its subscriber has been removed from the active subscriber registry, forcing clients to reconnect and replay instead of staying apparently healthy while receiving only `server_ping` events.
11
+
12
+ ### Tests
13
+ - Add server regressions covering registered subscribers independent of request-thread liveness and removed subscribers stopping instead of pinging forever; validate with targeted server/client/chat tests and the full offline CI suite (581 passed).
14
+
15
+ ## [0.1.57] - 2026-05-17
16
+ ### Fixed
17
+ - 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.
18
+ - Make `utils.Logger` create its parent directory before truncating the log file so first use works on fresh paths.
19
+
20
+ ### Changed
21
+ - Simplify small utility helpers for line-preserving truncation, message packing, and fenced stream pattern construction while preserving their existing behavior.
22
+
23
+ ### Tests
24
+ - Add focused regression coverage for the stream helpers, logger initialization, and utility formatting/truncation helpers; validate with the full offline CI suite (580 passed).
25
+
7
26
  ## [0.1.56] - 2026-05-17
8
27
  ### Changed
9
28
  - 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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-agent-framework
3
- Version: 0.1.56
3
+ Version: 0.1.58
4
4
  Summary: A lightweight event-driven Codex agent runtime.
5
5
  Author: Baptiste
6
6
  License-Expression: MIT
@@ -273,9 +273,12 @@ class AgentServer:
273
273
  return False
274
274
 
275
275
  def prune_subscribers(self):
276
- for subscriber, state in list(self.subscribers.items()):
277
- if not state.thread.is_alive():
278
- self.subscribers.pop(subscriber, None)
276
+ """Return active subscribers without pruning by request-thread liveness.
277
+
278
+ FastAPI may continue consuming a ``StreamingResponse`` after the
279
+ request handler thread that opened it is gone, so thread liveness is not
280
+ a reliable signal that an SSE subscriber is stale.
281
+ """
279
282
  return len(self.subscribers)
280
283
 
281
284
  def subscriber_count(self):
@@ -367,7 +370,7 @@ class AgentServer:
367
370
  try:
368
371
  payload = subscriber.get(timeout=0.25)
369
372
  except queue.Empty:
370
- if self.shutdown_requested.is_set():
373
+ if self.shutdown_requested.is_set() or subscriber not in self.subscribers:
371
374
  break
372
375
  payload = modict(type="server_ping")
373
376
  yield payload
@@ -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,),kwargs=dict(n=n),daemon=True)
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
- readers={}
61
- for k in self.queues:
62
- def reader():
63
- while (processed_chunk:=self.queues[k].get()) is not END:
64
- yield processed_chunk
65
- readers[k]=reader()
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
- def reader_true():
94
- while (chunk:=self.queue_true.get()) is not END:
95
- yield chunk
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
- return reader_true(),reader_false()
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
- Generates a regex pattern that matches text enclosed between a start and an end delimiter,
165
- The inner part is captured as the 'content' group (default).
166
- There can be other captured groups defined in the start and end patterns.
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 prepare_pattern(pattern):
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
- raise ValueError("`start` and `end` must be either a string or a regex Pattern object")
199
-
200
- start_regex = prepare_pattern(start)
201
- end_regex = prepare_pattern(end)
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
- return rf"\{key}"
208
- else:
209
- return rf"(?P={key})"
210
- end_regex = regex.sub(r"#(\w+)#", replace_placeholder, end_regex)
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
- def reader(key=key):
533
- while (value:=self.queues[key].get()) is not END:
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
- self.readers[key]=reader()
502
+
503
+ self.readers[key] = reader()
538
504
 
539
505
  def process(self,stream):
540
- self.queues=dict()
506
+ self.readers = {}
507
+ self.queues = {}
541
508
  self.done=False
542
509
  if self.defaults:
543
510
  self.maybe_init_readers(self.defaults)
@@ -230,69 +230,39 @@ def type_hierarchy(cls, root):
230
230
 
231
231
 
232
232
  def truncate(string, max_tokens=2000, start_line=1, model=None):
233
- """
234
- Truncate a string to a maximum number of tokens using line-based chunking.
235
-
236
- Args:
237
- string: The string to truncate
238
- max_tokens: Maximum number of tokens to keep
239
- start_line: The line number of the first line in the string (1-indexed).
240
- Used for skipping content and tracking line numbers in truncation messages.
241
-
242
- Returns:
243
- Truncated string with indication of removed content
244
- """
245
- # line-based pagination
233
+ """Return a line-preserving prefix that fits within ``max_tokens``."""
246
234
  lines = string.splitlines(keepends=True)
247
- if start_line > 1:
248
- if start_line > len(lines):
249
- return ""
250
- string = ''.join(lines[start_line - 1:])
251
- lines = string.splitlines(keepends=True)
252
-
253
- # Quick check - if already under limit after pagination, return as-is
254
- tokens = tokenize(string, model=model)
255
- if len(tokens) <= max_tokens:
256
- return string
235
+ if start_line > len(lines):
236
+ return "" if start_line > 1 else string
257
237
 
258
- # Calculate the actual last line number after skipping
259
- remaining_lines = len(lines)
260
-
261
- # Assemble lines up to max_tokens
262
- result_lines = []
263
- current_tokens = 0
238
+ lines = lines[max(0, start_line - 1):]
239
+ string = "".join(lines)
240
+ if len(tokenize(string, model=model)) <= max_tokens:
241
+ return string
264
242
 
243
+ kept_lines = []
244
+ kept_tokens = 0
265
245
  for line in lines:
266
246
  line_tokens = token_count(line, model=model)
267
- if current_tokens + line_tokens <= max_tokens:
268
- result_lines.append(line)
269
- current_tokens += line_tokens
270
- else:
247
+ if kept_tokens + line_tokens > max_tokens:
271
248
  break
272
-
273
- if result_lines:
274
- result = ''.join(result_lines)
275
- num_kept = len(result_lines)
276
-
277
- # Only show truncation message if we didn't include all lines
278
- if num_kept < remaining_lines:
279
- first_truncated = start_line + num_kept
280
- last_line = start_line + remaining_lines - 1
281
-
282
- if not result.endswith('\n'):
283
- result += '\n'
284
- result += f"\n...\n\n[Lines {first_truncated}-{last_line} truncated]\n"
285
-
286
- return result
287
- else:
288
- return f"[Lines {start_line}-{start_line + remaining_lines - 1} truncated - first line exceeds token limit]\n"
249
+ kept_lines.append(line)
250
+ kept_tokens += line_tokens
251
+
252
+ last_line = start_line + len(lines) - 1
253
+ if not kept_lines:
254
+ return f"[Lines {start_line}-{last_line} truncated - first line exceeds token limit]\n"
255
+
256
+ result = "".join(kept_lines)
257
+ first_truncated = start_line + len(kept_lines)
258
+ if first_truncated <= last_line:
259
+ if not result.endswith("\n"):
260
+ result += "\n"
261
+ result += f"\n...\n\n[Lines {first_truncated}-{last_line} truncated]\n"
262
+ return result
289
263
 
290
264
  def pack_msgs(messages):
291
- text = ''
292
- for message in messages:
293
- text += message.name + ':\n'
294
- text += message.content.strip() + '\n\n'
295
- return text
265
+ return "".join(f"{message.name}:\n{message.content.strip()}\n\n" for message in messages)
296
266
 
297
267
  def msg_token_count(msg, model=None):
298
268
  return msg.api_token_count(model=model)
@@ -468,8 +438,11 @@ def upload_file_to_folder(source, folder, name=None):
468
438
  class Logger:
469
439
 
470
440
  def __init__(self,file=None):
471
- self.file=file or root_join('logs',"log.txt")
472
- open(self.file,'w', encoding='utf-8').close()
441
+ self.file = os.fspath(file or root_join('logs', "log.txt"))
442
+ folder = os.path.dirname(self.file)
443
+ if folder:
444
+ os.makedirs(folder, exist_ok=True)
445
+ open(self.file, 'w', encoding='utf-8').close()
473
446
 
474
447
  def log(self,message):
475
448
  with open(self.file,'a',encoding="utf-8") as f:
@@ -0,0 +1 @@
1
+ __version__ = "0.1.58"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-agent-framework
3
- Version: 0.1.56
3
+ Version: 0.1.58
4
4
  Summary: A lightweight event-driven Codex agent runtime.
5
5
  Author: Baptiste
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codex-agent-framework"
7
- version = "0.1.56"
7
+ version = "0.1.58"
8
8
  description = "A lightweight event-driven Codex agent runtime."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.11"
@@ -513,22 +513,34 @@ def test_create_app_refuses_second_sse_client(monkeypatch):
513
513
  service.subscribers.clear()
514
514
 
515
515
 
516
- def test_agent_server_prunes_dead_event_subscribers():
516
+ def test_agent_server_keeps_registered_event_subscribers_independent_of_request_thread():
517
517
  agent = Agent(session="new")
518
518
  service = AgentServer(InProcessAgentRuntime(agent))
519
+ stream = service.subscribe()
520
+
521
+ assert next(stream).type == "server_subscribed"
522
+ service.prune_subscribers()
523
+ service.publish(modict(type="response_content_delta_event", delta="still attached"))
519
524
 
520
- class DeadThread:
521
- def is_alive(self):
522
- return False
525
+ assert next(stream).delta == "still attached"
526
+ stream.close()
527
+ assert len(service.subscribers) == 0
523
528
 
524
- service.subscribers[queue.Queue()] = modict(thread=DeadThread())
525
529
 
530
+ def test_agent_server_stops_stream_removed_from_subscribers():
531
+ agent = Agent(session="new")
532
+ service = AgentServer(InProcessAgentRuntime(agent))
526
533
  stream = service.subscribe()
534
+
535
+ assert next(stream).type == "server_subscribed"
536
+ service.subscribers.clear()
537
+
527
538
  try:
528
- assert next(stream).type == "server_subscribed"
529
- assert len(service.subscribers) == 1
530
- finally:
531
- stream.close()
539
+ next(stream)
540
+ except StopIteration:
541
+ pass
542
+ else:
543
+ raise AssertionError("Expected removed subscriber stream to stop instead of pinging forever")
532
544
 
533
545
 
534
546
  def test_agent_server_stop_closes_event_streams(monkeypatch):
@@ -6,7 +6,7 @@ from PIL import Image as PILImage
6
6
  from codex_agent.message import AssistantMessage, CompactionMessage, DeveloperMessage, InterruptMessage, ServerToolResponseMessage, ToolResultMessage, UserMessage
7
7
  from codex_agent import utils
8
8
  from codex_agent.image import ImageMessage, image_input_token_count
9
- from codex_agent.stream_utils import MappingStreamProcessor
9
+ from codex_agent.stream_utils import MappingStreamProcessor, MappingStreamSplitter, Splitter, Tee, fenced
10
10
  from codex_agent.utils import split_history_turns
11
11
  from codex_agent.utils import project_python_env
12
12
 
@@ -29,6 +29,28 @@ def test_split_history_turns_groups_complete_agent_loop():
29
29
  assert remainder == []
30
30
 
31
31
 
32
+ def test_pack_msgs_formats_named_messages():
33
+ class Message:
34
+ def __init__(self, name, content):
35
+ self.name = name
36
+ self.content = content
37
+
38
+ messages = [
39
+ Message("user", " hello "),
40
+ Message("assistant", "done"),
41
+ ]
42
+
43
+ assert utils.pack_msgs(messages) == "user:\nhello\n\nassistant:\ndone\n\n"
44
+
45
+
46
+ def test_logger_creates_parent_directory_and_truncates_file(tmp_path):
47
+ log_file = tmp_path / "logs" / "log.txt"
48
+
49
+ utils.Logger(file=log_file)
50
+
51
+ assert log_file.read_text(encoding="utf-8") == ""
52
+
53
+
32
54
  def test_project_python_env_prefers_running_interpreter():
33
55
  env = project_python_env(env={"PATH": "/usr/bin:/bin"}, executable="/opt/project-python/bin/python")
34
56
 
@@ -37,6 +59,30 @@ def test_project_python_env_prefers_running_interpreter():
37
59
  assert env["PYTHON_EXECUTABLE"] == "/opt/project-python/bin/python"
38
60
 
39
61
 
62
+ def test_tee_fans_out_stream_to_independent_readers():
63
+ first, second = Tee().tee(iter(["a", "b"]), n=2)
64
+
65
+ assert list(first) == ["a", "b"]
66
+ assert list(second) == ["a", "b"]
67
+
68
+
69
+ def test_splitter_routes_stream_to_independent_readers():
70
+ even, odd = Splitter(lambda number: number % 2 == 0).split(iter([1, 2, 3, 4]))
71
+
72
+ assert list(even) == [2, 4]
73
+ assert list(odd) == [1, 3]
74
+
75
+
76
+ def test_mapping_stream_splitter_can_be_reused():
77
+ splitter = MappingStreamSplitter(threaded=False)
78
+
79
+ first = splitter.split(iter([{"text": "a"}]))
80
+ second = splitter.split(iter([{"text": "b"}]))
81
+
82
+ assert list(first["text"]) == ["a"]
83
+ assert list(second["text"]) == ["b"]
84
+
85
+
40
86
  def test_mapping_stream_processor_is_direct_passthrough_without_processors():
41
87
  source = iter([{"content": "a"}, {"content": "b"}])
42
88
  processor = MappingStreamProcessor(processors={"content": []}, threaded=True)
@@ -47,6 +93,24 @@ def test_mapping_stream_processor_is_direct_passthrough_without_processors():
47
93
  assert list(result) == [{"content": "a"}, {"content": "b"}]
48
94
 
49
95
 
96
+ def test_fenced_reuses_numbered_capture_in_closing_pattern():
97
+ pattern = fenced(r"BEGIN (\w+)\n", r"\nEND #1#")
98
+
99
+ match = pattern.fullmatch("BEGIN tag\nhello\nEND tag")
100
+
101
+ assert match is not None
102
+ assert match.group("content") == "hello"
103
+
104
+
105
+ def test_fenced_reuses_named_capture_in_closing_pattern():
106
+ pattern = fenced(r"<(?P<tag>\w+)>", r"</#tag#>")
107
+
108
+ match = pattern.fullmatch("<note>hello</note>")
109
+
110
+ assert match is not None
111
+ assert match.group("content") == "hello"
112
+
113
+
50
114
  def test_read_document_content_adds_line_numbers_by_default(tmp_path):
51
115
  file = tmp_path / "sample.txt"
52
116
  file.write_text("alpha\nbeta\ngamma", encoding="utf-8")
@@ -390,6 +454,28 @@ def test_image_message_token_count_ignores_base64_text_volume():
390
454
  assert message.estimated_token_count["gpt-5"]["count"] == count
391
455
 
392
456
 
457
+ def test_truncate_returns_requested_page_when_under_token_limit(monkeypatch):
458
+ monkeypatch.setattr(utils, "tokenize", lambda text, model=None: list(text))
459
+ monkeypatch.setattr(utils, "token_count", lambda text, model=None: len(text))
460
+
461
+ assert utils.truncate("a\nb\nc\n", max_tokens=10, start_line=2) == "b\nc\n"
462
+
463
+
464
+ def test_truncate_appends_line_range_notice(monkeypatch):
465
+ monkeypatch.setattr(utils, "tokenize", lambda text, model=None: list(text))
466
+ monkeypatch.setattr(utils, "token_count", lambda text, model=None: len(text))
467
+
468
+ assert utils.truncate("aa\nbb\ncc\n", max_tokens=3) == "aa\n\n...\n\n[Lines 2-3 truncated]\n"
469
+
470
+
471
+ def test_truncate_reports_first_line_when_it_exceeds_limit(monkeypatch):
472
+ monkeypatch.setattr(utils, "tokenize", lambda text, model=None: list(text))
473
+ monkeypatch.setattr(utils, "token_count", lambda text, model=None: len(text))
474
+
475
+ assert utils.truncate("aaaa\nbb\n", max_tokens=2, start_line=3) == ""
476
+ assert utils.truncate("aaaa\nbb\n", max_tokens=2) == "[Lines 1-2 truncated - first line exceeds token limit]\n"
477
+
478
+
393
479
  def test_load_runtime_env_reads_runtime_dotenv(isolated_runtime_dir, monkeypatch):
394
480
  isolated_runtime_dir.mkdir(parents=True, exist_ok=True)
395
481
  env_file = isolated_runtime_dir / ".env"
@@ -1 +0,0 @@
1
- __version__ = "0.1.56"