codex-agent-framework 0.1.58__tar.gz → 0.1.59__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 (157) hide show
  1. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/CHANGELOG.md +13 -0
  2. {codex_agent_framework-0.1.58/codex_agent_framework.egg-info → codex_agent_framework-0.1.59}/PKG-INFO +2 -2
  3. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/agent.py +1 -4
  4. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/ai.py +10 -1
  5. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_commands.py +0 -2
  6. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/context/__init__.py +0 -2
  7. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/image_generation/__init__.py +1 -0
  8. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/config.py +1 -0
  9. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/context.py +69 -26
  10. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/message.py +85 -9
  11. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/sessions.py +18 -8
  12. codex_agent_framework-0.1.59/codex_agent/token_counter.py +115 -0
  13. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/tool.py +86 -2
  14. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/tui/app.py +5 -4
  15. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/tui/chat.py +6 -1
  16. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/tui/state.py +34 -8
  17. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/tui/widgets.py +23 -0
  18. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/utils.py +1 -1
  19. codex_agent_framework-0.1.59/codex_agent/version.py +1 -0
  20. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59/codex_agent_framework.egg-info}/PKG-INFO +2 -2
  21. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent_framework.egg-info/SOURCES.txt +2 -0
  22. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent_framework.egg-info/requires.txt +1 -1
  23. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/pyproject.toml +2 -2
  24. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_agent_context.py +81 -14
  25. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_chat.py +117 -64
  26. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_messages.py +21 -1
  27. codex_agent_framework-0.1.59/tests/test_token_counter.py +211 -0
  28. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_utils.py +11 -6
  29. codex_agent_framework-0.1.58/codex_agent/version.py +0 -1
  30. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/LICENSE +0 -0
  31. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/MANIFEST.in +0 -0
  32. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/README.md +0 -0
  33. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/__init__.py +0 -0
  34. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/__main__.py +0 -0
  35. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/agent_runtime.py +0 -0
  36. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/browser.py +0 -0
  37. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/__init__.py +0 -0
  38. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/bash/__init__.py +0 -0
  39. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/bash/requirements.txt +0 -0
  40. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/browser/__init__.py +0 -0
  41. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/browser/requirements.txt +0 -0
  42. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/content/__init__.py +0 -0
  43. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/content/requirements.txt +0 -0
  44. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/content/system.py +0 -0
  45. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/content/vision.py +0 -0
  46. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/desktop/__init__.py +0 -0
  47. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/desktop/requirements.txt +0 -0
  48. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/environment/__init__.py +0 -0
  49. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/environment/requirements.txt +0 -0
  50. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/files/__init__.py +0 -0
  51. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/files/requirements.txt +0 -0
  52. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/files/tools.py +0 -0
  53. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/image_generation/image_generation_system_prompt.txt +0 -0
  54. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/image_generation/requirements.txt +0 -0
  55. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/interface/__init__.py +0 -0
  56. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/interface/requirements.txt +0 -0
  57. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/memory/__init__.py +0 -0
  58. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/memory/requirements.txt +0 -0
  59. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/planner/__init__.py +0 -0
  60. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/planner/requirements.txt +0 -0
  61. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/python/__init__.py +0 -0
  62. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/python/requirements.txt +0 -0
  63. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/realtime/__init__.py +0 -0
  64. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/realtime/audio.py +0 -0
  65. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/realtime/bridge.py +0 -0
  66. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/realtime/manager.py +0 -0
  67. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/realtime/requirements.txt +0 -0
  68. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/realtime/session.py +0 -0
  69. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/scheduler/__init__.py +0 -0
  70. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/scheduler/requirements.txt +0 -0
  71. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/subagents/__init__.py +0 -0
  72. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/subagents/profiles/explorer.json +0 -0
  73. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/subagents/requirements.txt +0 -0
  74. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/tts/__init__.py +0 -0
  75. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/tts/requirements.txt +0 -0
  76. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/builtin_plugins/web_search/__init__.py +0 -0
  77. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/cli/__init__.py +0 -0
  78. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/cli/headless.py +0 -0
  79. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/cli/main.py +0 -0
  80. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/cli/output.py +0 -0
  81. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/cli/root.py +0 -0
  82. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/cli/runner.py +0 -0
  83. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/client.py +0 -0
  84. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/command.py +0 -0
  85. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/desktop.py +0 -0
  86. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/event.py +0 -0
  87. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/get_text/__init__.py +0 -0
  88. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/get_text/default_gitignore +0 -0
  89. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/get_text/get_text.py +0 -0
  90. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/get_text/simpler_get_text.py +0 -0
  91. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/hooks.py +0 -0
  92. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/image.py +0 -0
  93. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/latex.py +0 -0
  94. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/mainloop.py +0 -0
  95. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/plugin.py +0 -0
  96. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/prompts/system_prompt.txt +0 -0
  97. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/provider.py +0 -0
  98. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/runtime.py +0 -0
  99. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/scheduler.py +0 -0
  100. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/scripts/install-system-dependencies.sh +0 -0
  101. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/server/__init__.py +0 -0
  102. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/server/app.py +0 -0
  103. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/server/core.py +0 -0
  104. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/service.py +0 -0
  105. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/status.py +0 -0
  106. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/stream.py +0 -0
  107. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/stream_utils.py +0 -0
  108. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/tray.py +0 -0
  109. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/tui/__init__.py +0 -0
  110. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/tui/lifecycle.py +0 -0
  111. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/tui/log.py +0 -0
  112. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/tui/perf.py +0 -0
  113. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/tui/status_bar.py +0 -0
  114. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/tui/stream_debug.py +0 -0
  115. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/tui/style.py +0 -0
  116. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/tui/transcript.py +0 -0
  117. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent/worker.py +0 -0
  118. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent_framework.egg-info/dependency_links.txt +0 -0
  119. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent_framework.egg-info/entry_points.txt +0 -0
  120. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/codex_agent_framework.egg-info/top_level.txt +0 -0
  121. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/dependencies.txt +0 -0
  122. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/setup.cfg +0 -0
  123. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_agent.py +0 -0
  124. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_agent_audio_hooks.py +0 -0
  125. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_agent_commands.py +0 -0
  126. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_agent_desktop.py +0 -0
  127. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_agent_environment.py +0 -0
  128. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_agent_image_generation.py +0 -0
  129. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_agent_observe.py +0 -0
  130. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_agent_plugins.py +0 -0
  131. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_agent_providers_config.py +0 -0
  132. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_agent_realtime.py +0 -0
  133. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_agent_response.py +0 -0
  134. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_agent_runtime.py +0 -0
  135. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_agent_sessions.py +0 -0
  136. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_agent_subagents.py +0 -0
  137. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_agent_tools.py +0 -0
  138. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_agent_turns.py +0 -0
  139. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_ai.py +0 -0
  140. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_browser.py +0 -0
  141. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_builtin_config.py +0 -0
  142. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_cli.py +0 -0
  143. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_cli_headless.py +0 -0
  144. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_cli_root.py +0 -0
  145. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_client.py +0 -0
  146. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_events.py +0 -0
  147. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_get_text_browser.py +0 -0
  148. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_hooks.py +0 -0
  149. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_image_message.py +0 -0
  150. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_local_desktop.py +0 -0
  151. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_memory.py +0 -0
  152. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_planner.py +0 -0
  153. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_scheduler.py +0 -0
  154. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_server.py +0 -0
  155. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_service.py +0 -0
  156. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_tui.py +0 -0
  157. {codex_agent_framework-0.1.58 → codex_agent_framework-0.1.59}/tests/test_worker.py +0 -0
@@ -4,6 +4,19 @@ 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.59] - 2026-05-18
8
+ ### Changed
9
+ - Make local context sizing rely on cached local token estimation for messages and tool schemas, with bounded smoothing against observed backend `usage.input_tokens` so status and auto-compaction decisions track reality more closely without a runtime token-counter dependency.
10
+ - Default session compaction to `gpt-5.4-mini` and stop injecting the main agent instructions into compaction requests unless explicit compaction instructions are provided.
11
+ - Bake stable token-count estimates for server tools and keep compact-session usage metadata available for later context accounting.
12
+
13
+ ### Fixed
14
+ - Preserve compaction usage data end-to-end in agent session state so compacted history can reuse backend-observed token accounting when available.
15
+ - Raise the minimum SDK dependency to `codex-backend-sdk>=0.3.5` so compact response usage is available to the agent runtime.
16
+
17
+ ### Tests
18
+ - Add and update focused coverage for local token estimation, correction coefficient behavior, compaction defaults/overrides, compact-session usage accounting, and baked tool token counts.
19
+
7
20
  ## [0.1.58] - 2026-05-17
8
21
  ### Fixed
9
22
  - 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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codex-agent-framework
3
- Version: 0.1.58
3
+ Version: 0.1.59
4
4
  Summary: A lightweight event-driven Codex agent runtime.
5
5
  Author: Baptiste
6
6
  License-Expression: MIT
@@ -16,7 +16,7 @@ Description-Content-Type: text/markdown
16
16
  License-File: LICENSE
17
17
  Requires-Dist: audioop-lts; python_version >= "3.13"
18
18
  Requires-Dist: beautifulsoup4
19
- Requires-Dist: codex-backend-sdk>=0.3.1
19
+ Requires-Dist: codex-backend-sdk>=0.3.5
20
20
  Requires-Dist: fastapi
21
21
  Requires-Dist: filetype
22
22
  Requires-Dist: modict
@@ -461,10 +461,7 @@ class Agent:
461
461
  self.status_manager.invalidate_context_cache()
462
462
  messages = self.context_manager.get_context()
463
463
  if self.context_manager.should_auto_compact(messages):
464
- self.compact_session(
465
- model=self.config.model,
466
- instructions=self.config_manager.system_message().content,
467
- )
464
+ self.compact_session()
468
465
  messages = self.context_manager.get_context()
469
466
 
470
467
  message = self.ai.run(
@@ -248,6 +248,10 @@ class AIClient:
248
248
 
249
249
  try:
250
250
  response_params = self._to_codex_response_params(params)
251
+ raw_input_estimate = self.agent.context_manager.raw_response_input_token_count(
252
+ params.get("messages", []),
253
+ response_tools=params.get("tools") or [],
254
+ )
251
255
  response = self.codex_client.responses.create(**response_params, stream=True)
252
256
  except Exception as e:
253
257
  if self.is_bad_request_error(e):
@@ -265,8 +269,13 @@ class AIClient:
265
269
  self.agent.mainloop_manager.touch_activity("response_stream")
266
270
  if getattr(event, "type", None) == "response.completed":
267
271
  completed = self._event_response(event)
272
+ usage = modict(completed.get("usage") or {})
273
+ self.agent.context_manager.apply_token_count_correction(
274
+ raw_input_estimate,
275
+ usage.get("input_tokens"),
276
+ )
268
277
  self.agent.status_manager.cache_api_status(
269
- usage=modict(completed.get("usage") or {}),
278
+ usage=usage,
270
279
  model=completed.get("model") or response_params.get("model"),
271
280
  quota=self.get_quota_usage(),
272
281
  )
@@ -38,8 +38,6 @@ def compact(max_compacted_turns=None):
38
38
  agent = get_agent()
39
39
  max_compacted_turns = None if max_compacted_turns in (None, "", "all") else int(max_compacted_turns)
40
40
  message = agent.compact_session(
41
- model=agent.config.model,
42
- instructions=agent.config_manager.system_message().content,
43
41
  max_compacted_turns=max_compacted_turns,
44
42
  )
45
43
  if message is None:
@@ -117,8 +117,6 @@ Tool result wrappers expose stable `wrapper_id` values in their XML representati
117
117
  max_compacted_turns = int(max_compacted_turns)
118
118
  before = self.context_public_status()
119
119
  message = self.agent.compact_session(
120
- model=self.agent.config.model,
121
- instructions=self.agent.config_manager.system_message().content,
122
120
  max_compacted_turns=max_compacted_turns,
123
121
  )
124
122
  after = self.context_public_status()
@@ -9,6 +9,7 @@ from ...utils import text_content
9
9
 
10
10
  class ImageGenerationTool(ServerTool):
11
11
  type = "image_generation"
12
+ baked_token_count = 342
12
13
  output_format = "png"
13
14
  size = None
14
15
  quality = None
@@ -81,6 +81,7 @@ class Config(modict):
81
81
  class AgentConfig(Config):
82
82
 
83
83
  model = "gpt-5.4"
84
+ compaction_model = "gpt-5.4-mini"
84
85
  system = DEFAULT_SYSTEM_PROMPT
85
86
  auto_proceed = True
86
87
  openai_api_key = None
@@ -3,12 +3,17 @@ from modict import modict
3
3
  from .image import ImageMessage
4
4
  from .message import Message, ProviderMessage, SystemMessage, ToolResultWrapper
5
5
  from .tool import reset_current_agent, set_current_agent
6
- from .utils import sort, token_count_payload, truncate
6
+ from .utils import sort, token_count, token_count_payload, truncate
7
7
 
8
8
 
9
9
  class ContextManager:
10
+ TOKEN_COUNT_COEF_MIN = 0.5
11
+ TOKEN_COUNT_COEF_MAX = 2.0
12
+ TOKEN_COUNT_COEF_ALPHA = 0.8
13
+
10
14
  def __init__(self, agent):
11
15
  self.agent = agent
16
+ self.token_count_coef = 1.0
12
17
 
13
18
  def provider_context_wrapper(self, provider_name, content):
14
19
  return f'<context_provider name="{provider_name}">\n{content}\n</context_provider>'
@@ -149,13 +154,6 @@ class ContextManager:
149
154
  compactable_messages = 0
150
155
  compactable_tokens = 0
151
156
  token_count_source = "local_estimate"
152
- try:
153
- current_count = self.agent.ai.count_input_tokens(
154
- **self.response_input_token_payload(formatted_context, response_tools=response_tools)
155
- )
156
- token_count_source = "openai_input_tokens"
157
- except Exception:
158
- pass
159
157
  input_token_limit = self.agent.config.input_token_limit
160
158
  used_token_percent = (current_count / input_token_limit * 100) if input_token_limit else 0
161
159
  sent_session_message_percent = (
@@ -216,11 +214,25 @@ class ContextManager:
216
214
  return msg.get("turns_left", -1) != 0
217
215
 
218
216
  def response_tools_token_count(self, response_tools=None):
217
+ model = self.agent.config.model
219
218
  if response_tools is None:
220
- response_tools = self.agent.tools_manager.get_response_tools()
219
+ return self.agent.tools_manager.response_tools_token_count(model=model)
221
220
  if not response_tools:
222
221
  return 0
223
- return token_count_payload({"tools": response_tools}, model=self.agent.config.model)
222
+ tools = self.agent.tools_manager.get_tools()
223
+ by_name = {
224
+ (tool.get("name") or tool.get("type")): tool
225
+ for tool in tools
226
+ }
227
+ total = 0
228
+ for spec in response_tools:
229
+ name = spec.get("name") or spec.get("type")
230
+ tool = by_name.get(name)
231
+ if tool is not None:
232
+ total += tool.count_tokens(model=model)
233
+ else:
234
+ total += token_count_payload(spec, model=model)
235
+ return int(total)
224
236
 
225
237
  def response_input_token_payload(self, messages, response_tools=None):
226
238
  instructions = []
@@ -280,23 +292,54 @@ class ContextManager:
280
292
 
281
293
  return sanitize(payload)
282
294
 
283
- def response_input_token_count(self, messages, response_tools=None, authoritative=False):
284
- payload = self.response_input_token_payload(messages, response_tools=response_tools)
285
- if authoritative:
286
- try:
287
- return self.agent.ai.count_input_tokens(**payload)
288
- except Exception:
289
- pass
290
- media_tokens = sum(
291
- msg.api_token_count(model=self.agent.config.model)
292
- for msg in messages
293
- if isinstance(msg, ImageMessage)
294
- )
295
- local_tokens = token_count_payload(
296
- self.local_token_count_payload(payload),
297
- model=self.agent.config.model,
295
+ def estimate_token_count(self, raw_count):
296
+ raw_count = int(raw_count or 0)
297
+ if raw_count <= 0:
298
+ return 0
299
+ return max(0, int(round(raw_count * self.token_count_coef)))
300
+
301
+ def apply_token_count_correction(self, raw_estimate, observed_input_tokens):
302
+ raw_estimate = int(raw_estimate or 0)
303
+ observed_input_tokens = int(observed_input_tokens or 0)
304
+ if raw_estimate <= 0 or observed_input_tokens <= 0:
305
+ return self.token_count_coef
306
+ observed_ratio = observed_input_tokens / raw_estimate
307
+ observed_ratio = max(self.TOKEN_COUNT_COEF_MIN, min(self.TOKEN_COUNT_COEF_MAX, observed_ratio))
308
+ new_coef = (
309
+ self.token_count_coef * self.TOKEN_COUNT_COEF_ALPHA
310
+ + observed_ratio * (1.0 - self.TOKEN_COUNT_COEF_ALPHA)
298
311
  )
299
- return local_tokens + media_tokens
312
+ self.token_count_coef = max(self.TOKEN_COUNT_COEF_MIN, min(self.TOKEN_COUNT_COEF_MAX, new_coef))
313
+ return self.token_count_coef
314
+
315
+ def raw_response_input_token_count(self, messages, response_tools=None):
316
+ model = self.agent.config.model
317
+ instructions = []
318
+ history_messages = []
319
+ in_instruction_prefix = True
320
+ for msg in messages:
321
+ if in_instruction_prefix and isinstance(msg, SystemMessage):
322
+ instruction = msg.instruction_text()
323
+ if instruction:
324
+ instructions.append(instruction)
325
+ continue
326
+ in_instruction_prefix = False
327
+ history_messages.append(msg)
328
+
329
+ instruction_tokens = 0
330
+ if instructions:
331
+ instruction_tokens = token_count("\n\n".join(instructions), model=model)
332
+
333
+ history_tokens = sum(msg.count_tokens(model=model) for msg in history_messages)
334
+ tool_tokens = self.response_tools_token_count(response_tools=response_tools)
335
+ reasoning = self.agent.config.get("reasoning")
336
+ reasoning_tokens = token_count_payload({"reasoning": modict(reasoning)}, model=model) if reasoning else 0
337
+ tool_choice_tokens = token_count_payload({"tool_choice": "auto"}, model=model) if response_tools else 0
338
+ return instruction_tokens + history_tokens + tool_tokens + reasoning_tokens + tool_choice_tokens
339
+
340
+ def response_input_token_count(self, messages, response_tools=None):
341
+ raw_count = self.raw_response_input_token_count(messages, response_tools=response_tools)
342
+ return self.estimate_token_count(raw_count)
300
343
 
301
344
  def should_auto_compact(self, messages):
302
345
  if not self.agent.config.get("auto_compact", True):
@@ -36,6 +36,7 @@ class Message(modict):
36
36
  timestamp = modict.factory(lambda: timestamp())
37
37
  turns_left = -1
38
38
  embedding = None
39
+ token_count = modict.factory(dict)
39
40
  estimated_token_count = modict.factory(dict)
40
41
 
41
42
  def __init__(self, *args, **kwargs):
@@ -91,24 +92,51 @@ class Message(modict):
91
92
  def to_response_format(self):
92
93
  raise NotImplementedError(f"{type(self).__name__} must implement to_response_format()")
93
94
 
94
- def api_token_count(self, model=None):
95
+ def token_count_cache(self):
96
+ cache = self.get("token_count")
97
+ if isinstance(cache, dict) and cache:
98
+ return cache
99
+ legacy = self.get("estimated_token_count")
100
+ if isinstance(legacy, dict) and legacy:
101
+ self.token_count = dict(legacy)
102
+ return self.token_count
103
+ self.token_count = {}
104
+ return self.token_count
105
+
106
+ def cache_token_count(self, count, model=None):
107
+ model_key = str(model or "gpt-5")
108
+ cache = self.token_count_cache()
109
+ cache[model_key] = {
110
+ "fingerprint": self.api_token_fingerprint(),
111
+ "count": int(count or 0),
112
+ }
113
+ self.token_count = cache
114
+ self.estimated_token_count = dict(cache)
115
+ return int(count or 0)
116
+
117
+ def count_tokens(self, model=None):
95
118
  model_key = str(model or "gpt-5")
96
119
  fingerprint = self.api_token_fingerprint()
97
- cache = self.get("estimated_token_count") or {}
120
+ cache = self.token_count_cache()
98
121
  cached = cache.get(model_key)
99
122
  if isinstance(cached, dict) and cached.get("fingerprint") == fingerprint:
100
123
  return int(cached.get("count") or 0)
101
124
 
102
- count = self.compute_api_token_count(model=model)
103
- cache[model_key] = {"fingerprint": fingerprint, "count": count}
104
- self.estimated_token_count = cache
105
- return count
125
+ count = int(self.compute_api_token_count(model=model) or 0)
126
+ return self.cache_token_count(count, model=model)
127
+
128
+ def api_token_count(self, model=None):
129
+ return self.count_tokens(model=model)
106
130
 
107
131
  @modict.computed()
108
- def effective_api_token_count(self):
132
+ def effective_token_count(self):
109
133
  if self.get("turns_left", -1) == 0:
110
134
  return 0
111
- return self.api_token_count()
135
+ return self.count_tokens()
136
+
137
+ @modict.computed()
138
+ def effective_api_token_count(self):
139
+ return self.effective_token_count
112
140
 
113
141
  def api_token_fingerprint(self):
114
142
  payload = {
@@ -120,10 +148,21 @@ class Message(modict):
120
148
  text = json.dumps(payload, ensure_ascii=False, sort_keys=True, separators=(",", ":"), default=str)
121
149
  return hashlib.sha256(text.encode("utf-8")).hexdigest()
122
150
 
151
+ def token_count_request(self, model=None):
152
+ item = self.to_response_format()
153
+ if item is None:
154
+ return None
155
+ if isinstance(item, list):
156
+ return {"model": str(model or "gpt-5"), "input": item}
157
+ return {"model": str(model or "gpt-5"), "input": [item]}
158
+
159
+ def token_count_payload(self):
160
+ return self.to_response_format()
161
+
123
162
  def compute_api_token_count(self, model=None):
124
163
  from .utils import token_count, token_count_payload
125
164
 
126
- item = self.to_response_format()
165
+ item = self.token_count_payload()
127
166
  if item is None:
128
167
  return token_count(str(self.get("content") or ""), model=model)
129
168
  return token_count_payload(item, model=model)
@@ -190,6 +229,9 @@ class SystemMessage(TextMessage):
190
229
  parts.append(str(part))
191
230
  return "\n".join(part for part in parts if part)
192
231
 
232
+ def token_count_request(self, model=None):
233
+ return {"model": str(model or "gpt-5"), "instructions": self.instruction_text()}
234
+
193
235
  def compute_api_token_count(self, model=None):
194
236
  from .utils import token_count
195
237
 
@@ -218,6 +260,7 @@ class CompactionMessage(DeveloperMessage):
218
260
  content = "Previous conversation context was compacted by the backend."
219
261
  response_id = ""
220
262
  summaries = modict.factory(list)
263
+ usage = modict.factory(modict)
221
264
  compacted_message_count = 0
222
265
 
223
266
  def __init__(self, *args, **kwargs):
@@ -238,6 +281,39 @@ class CompactionMessage(DeveloperMessage):
238
281
  def to_response_format(self):
239
282
  return list(self.get("summaries") or [])
240
283
 
284
+ def observed_token_count(self):
285
+ usage = self.get("usage") or {}
286
+ if not isinstance(usage, dict):
287
+ return None
288
+ for key in ("input_tokens", "max_input_tokens"):
289
+ value = usage.get(key)
290
+ if isinstance(value, int) and value >= 0:
291
+ return value
292
+ return None
293
+
294
+ def count_tokens(self, model=None):
295
+ observed = self.observed_token_count()
296
+ if observed is not None:
297
+ return int(observed)
298
+ return super().count_tokens(model=model)
299
+
300
+ def token_count_request(self, model=None):
301
+ if self.observed_token_count() is not None:
302
+ return None
303
+ payload = self.token_count_payload()
304
+ if not payload:
305
+ return {"model": str(model or "gpt-5"), "input": []}
306
+ return {"model": str(model or "gpt-5"), "input": payload}
307
+
308
+ def token_count_payload(self):
309
+ sanitized = []
310
+ for item in self.get("summaries") or []:
311
+ entry = dict(item)
312
+ if entry.get("type") == "compaction_summary" and entry.get("encrypted_content"):
313
+ entry["encrypted_content"] = "<encrypted_compaction_summary>"
314
+ sanitized.append(entry)
315
+ return sanitized
316
+
241
317
  def as_string_attrs(self):
242
318
  attrs = self.extract('name', 'type', 'timestamp')
243
319
  attrs["response_id"] = self.get("response_id")
@@ -151,7 +151,7 @@ class AgentSession(modict):
151
151
  self,
152
152
  client=None,
153
153
  model="gpt-5.4",
154
- instructions="",
154
+ instructions=None,
155
155
  max_input_tokens=DEFAULT_COMPACTION_MAX_INPUT_TOKENS,
156
156
  max_compacted_turns=None,
157
157
  ):
@@ -165,15 +165,26 @@ class AgentSession(modict):
165
165
  return None
166
166
 
167
167
  history = self.messages_to_response_history(plan.history_messages)
168
- client = client or CodexClient(model=model, instructions=instructions).authenticate()
169
- result = client.responses.compact(
168
+ client_kwargs = modict(model=model)
169
+ if instructions is not None:
170
+ client_kwargs.instructions = instructions
171
+ client = client or CodexClient(**client_kwargs).authenticate()
172
+ compact_params = modict(
170
173
  input=history,
171
174
  model=model,
172
- instructions=instructions,
173
175
  )
176
+ if instructions is not None:
177
+ compact_params.instructions = instructions
178
+ result = client.responses.compact(**compact_params)
179
+ usage = getattr(result, "usage", None)
180
+ if hasattr(usage, "model_dump"):
181
+ usage = usage.model_dump()
182
+ elif usage is not None and not isinstance(usage, dict):
183
+ usage = dict(usage)
174
184
  message = CompactionMessage(
175
185
  response_id=result.id,
176
186
  summaries=CompactionMessage.summary_items(result.output),
187
+ usage=usage or {},
177
188
  compacted_message_count=len(compacted_messages),
178
189
  compacted_turn_count=plan.compacted_turn_count,
179
190
  preserved_turn_count=plan.preserved_turn_count,
@@ -797,9 +808,7 @@ class SessionsManager:
797
808
  max_compacted_turns=max_compacted_turns,
798
809
  ).wait().result
799
810
 
800
- model = model or self.agent.config.model
801
- if instructions is None:
802
- instructions = self.agent.config_manager.system_message().content
811
+ model = model or self.agent.config.get("compaction_model") or self.agent.config.model
803
812
  max_input_tokens = self.agent.config.get(
804
813
  "max_input_tokens",
805
814
  DEFAULT_COMPACTION_MAX_INPUT_TOKENS,
@@ -821,9 +830,10 @@ class SessionsManager:
821
830
  )
822
831
  compact_kwargs = modict(
823
832
  model=model,
824
- instructions=instructions,
825
833
  max_input_tokens=max_input_tokens,
826
834
  )
835
+ if instructions is not None:
836
+ compact_kwargs.instructions = instructions
827
837
  if max_compacted_turns is not None:
828
838
  compact_kwargs.max_compacted_turns = max_compacted_turns
829
839
  if client is not None:
@@ -0,0 +1,115 @@
1
+ class TokenCounter:
2
+ """Count tokens for a single object using minimal Responses input_tokens requests.
3
+
4
+ The counter is object-oriented: each supported object provides a
5
+ ``token_count_request(model=...)`` projection and stores its own cached
6
+ result locally in ``token_count``.
7
+ """
8
+
9
+ def __init__(self, ai, default_model="gpt-5", baseline_text="PING"):
10
+ self.ai = ai
11
+ self.default_model = default_model
12
+ self.baseline_text = baseline_text
13
+ self._baseline_counts = {}
14
+
15
+ def count_object(self, obj, model=None):
16
+ model = str(model or self.default_model or "gpt-5")
17
+
18
+ cached = self._cached_count(obj, model=model)
19
+ if cached is not None:
20
+ return cached
21
+
22
+ observed = self._observed_count(obj)
23
+ if observed is not None:
24
+ return self._store_count(obj, observed, model=model)
25
+
26
+ request = getattr(obj, "token_count_request", lambda model=None: None)(model=model)
27
+ if request is None:
28
+ if hasattr(obj, "count_tokens"):
29
+ return int(obj.count_tokens(model=model))
30
+ raise TypeError(f"{type(obj).__name__} does not support token counting")
31
+
32
+ if self._needs_baseline(request):
33
+ baseline = self._baseline_request(model=model)
34
+ baseline_count = self._baseline_count(model=model)
35
+ payload = dict(request)
36
+ payload["input"] = baseline["input"]
37
+ count = int(self.ai.count_input_tokens(**payload)) - baseline_count
38
+ else:
39
+ count = int(self.ai.count_input_tokens(**request))
40
+ return self._store_count(obj, count, model=model)
41
+
42
+ def hydrate(self, objects, model=None):
43
+ return [self.count_object(obj, model=model) for obj in objects]
44
+
45
+ def _baseline_request(self, model=None):
46
+ return {
47
+ "model": str(model or self.default_model or "gpt-5"),
48
+ "input": [{
49
+ "type": "message",
50
+ "role": "user",
51
+ "content": [{"type": "input_text", "text": self.baseline_text}],
52
+ }],
53
+ }
54
+
55
+ def _baseline_count(self, model=None):
56
+ model_key = str(model or self.default_model or "gpt-5")
57
+ cached = self._baseline_counts.get(model_key)
58
+ if cached is not None:
59
+ return int(cached)
60
+ count = int(self.ai.count_input_tokens(**self._baseline_request(model=model_key)))
61
+ self._baseline_counts[model_key] = count
62
+ return count
63
+
64
+ def _needs_baseline(self, request):
65
+ if not isinstance(request, dict):
66
+ return False
67
+ payload_input = request.get("input")
68
+ if payload_input:
69
+ return False
70
+ return True
71
+
72
+ def _observed_count(self, obj):
73
+ observed = getattr(obj, "observed_token_count", None)
74
+ if callable(observed):
75
+ value = observed()
76
+ if value is not None:
77
+ return int(value)
78
+ return None
79
+
80
+ def _fingerprint(self, obj):
81
+ if hasattr(obj, "api_token_fingerprint"):
82
+ return obj.api_token_fingerprint()
83
+ if hasattr(obj, "response_tool_fingerprint"):
84
+ return obj.response_tool_fingerprint()
85
+ return None
86
+
87
+ def _cache_map(self, obj):
88
+ token_count_cache = getattr(obj, "token_count_cache", None)
89
+ if callable(token_count_cache):
90
+ return token_count_cache()
91
+ getter = getattr(obj, "get", None)
92
+ if callable(getter):
93
+ cache = getter("token_count") or {}
94
+ return cache if isinstance(cache, dict) else {}
95
+ return {}
96
+
97
+ def _cached_count(self, obj, model=None):
98
+ model_key = str(model or self.default_model or "gpt-5")
99
+ fingerprint = self._fingerprint(obj)
100
+ cache = self._cache_map(obj)
101
+ cached = cache.get(model_key)
102
+ if not isinstance(cached, dict):
103
+ return None
104
+ if fingerprint is not None and cached.get("fingerprint") != fingerprint:
105
+ return None
106
+ if "count" not in cached:
107
+ return None
108
+ return int(cached.get("count") or 0)
109
+
110
+ def _store_count(self, obj, count, model=None):
111
+ count = int(count or 0)
112
+ cache_method = getattr(obj, "cache_token_count", None)
113
+ if callable(cache_method):
114
+ return int(cache_method(count, model=model))
115
+ return count