code-puppy 0.0.910__tar.gz → 0.0.911__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 (328) hide show
  1. {code_puppy-0.0.910 → code_puppy-0.0.911}/PKG-INFO +1 -1
  2. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/_builder.py +1 -1
  3. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/_compaction.py +19 -1
  4. code_puppy-0.0.911/code_puppy/agents/_model_message_transform.py +130 -0
  5. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/capabilities/compaction.py +37 -5
  6. code_puppy-0.0.911/code_puppy/context_accounting.py +154 -0
  7. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/model_switching.py +5 -2
  8. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/plugins/__init__.py +9 -0
  9. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/token_usage.py +67 -48
  10. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/subagent_invocation.py +1 -1
  11. {code_puppy-0.0.910 → code_puppy-0.0.911}/pyproject.toml +1 -1
  12. code_puppy-0.0.910/code_puppy/agents/_model_message_transform.py +0 -31
  13. {code_puppy-0.0.910 → code_puppy-0.0.911}/.gitignore +0 -0
  14. {code_puppy-0.0.910 → code_puppy-0.0.911}/LICENSE +0 -0
  15. {code_puppy-0.0.910 → code_puppy-0.0.911}/README.md +0 -0
  16. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/__init__.py +0 -0
  17. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/__main__.py +0 -0
  18. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/_plugin_i18n_lifecycle.py +0 -0
  19. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/_pydantic_tool_helpers.py +0 -0
  20. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/_pydantic_video_patch.py +0 -0
  21. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agent_completion_inbox.py +0 -0
  22. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agent_execution_context.py +0 -0
  23. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/__init__.py +0 -0
  24. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/_code_mode.py +0 -0
  25. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/_code_mode_guidance.py +0 -0
  26. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/_diagnostics.py +0 -0
  27. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/_foreign_thinking.py +0 -0
  28. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/_history.py +0 -0
  29. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/_key_listeners.py +0 -0
  30. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/_non_streaming_render.py +0 -0
  31. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/_output_limits.py +0 -0
  32. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/_run_signals.py +0 -0
  33. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/_runtime.py +0 -0
  34. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/_steer_processor.py +0 -0
  35. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/_subagent_recursion.py +0 -0
  36. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/_windows_console.py +0 -0
  37. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/_wire_tool_names.py +0 -0
  38. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/agent_code_puppy.py +0 -0
  39. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/agent_creator_agent.py +0 -0
  40. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/agent_helios.py +0 -0
  41. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/agent_manager.py +0 -0
  42. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/agent_model_judge.py +0 -0
  43. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/agent_planning.py +0 -0
  44. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/agent_qa_kitten.py +0 -0
  45. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/agent_web_retriever.py +0 -0
  46. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/base_agent.py +0 -0
  47. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/event_stream_handler.py +0 -0
  48. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/json_agent.py +0 -0
  49. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/retry_checkpoint.py +0 -0
  50. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/retry_profiles.py +0 -0
  51. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/run_stats.py +0 -0
  52. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/smooth_stream.py +0 -0
  53. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/stream_status.py +0 -0
  54. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/subagent_stream_handler.py +0 -0
  55. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/agents/thinking_output.py +0 -0
  56. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/asyncio_cleanup.py +0 -0
  57. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/atomic_io.py +0 -0
  58. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/atomic_json.py +0 -0
  59. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/ca_bundle.py +0 -0
  60. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/callbacks.py +0 -0
  61. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/capabilities/__init__.py +0 -0
  62. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/capabilities/eager_timing.py +0 -0
  63. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/chatgpt_codex_client.py +0 -0
  64. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/claude_cache_client.py +0 -0
  65. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/claude_oauth_transport.py +0 -0
  66. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/claude_thinking_display.py +0 -0
  67. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/cli_runner.py +0 -0
  68. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/__init__.py +0 -0
  69. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/add_model_details.py +0 -0
  70. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/add_model_menu.py +0 -0
  71. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/add_model_menus.py +0 -0
  72. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/add_model_vllm.py +0 -0
  73. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/agent_menu.py +0 -0
  74. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/attachments.py +0 -0
  75. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/autosave_menu.py +0 -0
  76. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/autosave_search.py +0 -0
  77. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/clipboard.py +0 -0
  78. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/colors_menu.py +0 -0
  79. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/command_handler.py +0 -0
  80. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/command_registry.py +0 -0
  81. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/completers.py +0 -0
  82. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/completion_cache.py +0 -0
  83. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/config_apply.py +0 -0
  84. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/config_commands.py +0 -0
  85. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/core_commands.py +0 -0
  86. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/file_index.py +0 -0
  87. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/file_path_completion.py +0 -0
  88. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/help_catalog.py +0 -0
  89. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/help_overlay.py +0 -0
  90. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/image_utils.py +0 -0
  91. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/load_context_completion.py +0 -0
  92. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/mcp/__init__.py +0 -0
  93. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/mcp/base.py +0 -0
  94. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/mcp/catalog_server_installer.py +0 -0
  95. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/mcp/custom_server_form.py +0 -0
  96. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/mcp/custom_server_installer.py +0 -0
  97. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/mcp/edit_command.py +0 -0
  98. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/mcp/handler.py +0 -0
  99. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/mcp/help_command.py +0 -0
  100. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/mcp/install_command.py +0 -0
  101. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/mcp/install_menu.py +0 -0
  102. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/mcp/list_command.py +0 -0
  103. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/mcp/logs_command.py +0 -0
  104. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/mcp/mcp_servers_store.py +0 -0
  105. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/mcp/remove_command.py +0 -0
  106. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/mcp/restart_command.py +0 -0
  107. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/mcp/search_command.py +0 -0
  108. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/mcp/silence_warning_command.py +0 -0
  109. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/mcp/start_all_command.py +0 -0
  110. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/mcp/start_command.py +0 -0
  111. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/mcp/status_command.py +0 -0
  112. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/mcp/stop_all_command.py +0 -0
  113. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/mcp/stop_command.py +0 -0
  114. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/mcp/trust_command.py +0 -0
  115. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/mcp/utils.py +0 -0
  116. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/mcp/wizard_utils.py +0 -0
  117. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/mcp_binding_menu.py +0 -0
  118. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/mcp_completion.py +0 -0
  119. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/menu_session.py +0 -0
  120. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/model_picker_completion.py +0 -0
  121. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/model_settings_defs.py +0 -0
  122. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/model_settings_menu.py +0 -0
  123. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/onboarding_slides.py +0 -0
  124. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/onboarding_wizard.py +0 -0
  125. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/pagination.py +0 -0
  126. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/pin_command_completion.py +0 -0
  127. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/refresh_models.py +0 -0
  128. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/session_browser.py +0 -0
  129. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/session_browser_data.py +0 -0
  130. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/session_commands.py +0 -0
  131. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/set_menu.py +0 -0
  132. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/set_menu_catalog.py +0 -0
  133. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/set_menu_schema.py +0 -0
  134. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/set_menu_settings.py +0 -0
  135. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/set_menu_shims.py +0 -0
  136. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/set_menu_values.py +0 -0
  137. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/shell_passthrough.py +0 -0
  138. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/skills_completion.py +0 -0
  139. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/tui_style.py +0 -0
  140. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/uc_menu.py +0 -0
  141. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/command_line/utils.py +0 -0
  142. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/config.py +0 -0
  143. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/config_file.py +0 -0
  144. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/diagnostics.py +0 -0
  145. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/error_logging.py +0 -0
  146. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/events/__init__.py +0 -0
  147. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/events/bridge.py +0 -0
  148. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/file_completion_io.py +0 -0
  149. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/file_completion_tokens.py +0 -0
  150. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/gemini_code_assist.py +0 -0
  151. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/gemini_common.py +0 -0
  152. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/gemini_model.py +0 -0
  153. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/hook_engine/README.md +0 -0
  154. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/hook_engine/__init__.py +0 -0
  155. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/hook_engine/aliases.py +0 -0
  156. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/hook_engine/engine.py +0 -0
  157. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/hook_engine/executor.py +0 -0
  158. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/hook_engine/matcher.py +0 -0
  159. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/hook_engine/models.py +0 -0
  160. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/hook_engine/registry.py +0 -0
  161. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/hook_engine/validator.py +0 -0
  162. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/http_retry.py +0 -0
  163. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/http_utils.py +0 -0
  164. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/httpx2_utils.py +0 -0
  165. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/i18n/__init__.py +0 -0
  166. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/i18n/audit.py +0 -0
  167. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/i18n/catalog.py +0 -0
  168. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/i18n/formats.py +0 -0
  169. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/i18n/interpolation.py +0 -0
  170. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/i18n/locale.py +0 -0
  171. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/i18n/locales/en-US.json +0 -0
  172. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/i18n/locales/es.json +0 -0
  173. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/i18n/locales/fr-CA.json +0 -0
  174. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/i18n/plugin_catalog.py +0 -0
  175. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/i18n/plurals.py +0 -0
  176. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/i18n/pseudo.py +0 -0
  177. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/i18n/translate.py +0 -0
  178. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/kennel_provider.py +0 -0
  179. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/keymap.py +0 -0
  180. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/list_filtering.py +0 -0
  181. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/main.py +0 -0
  182. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/mcp_/__init__.py +0 -0
  183. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/mcp_/agent_bindings.py +0 -0
  184. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/mcp_/async_lifecycle.py +0 -0
  185. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/mcp_/blocking_startup.py +0 -0
  186. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/mcp_/circuit_breaker.py +0 -0
  187. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/mcp_/config_wizard.py +0 -0
  188. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/mcp_/dashboard.py +0 -0
  189. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/mcp_/error_isolation.py +0 -0
  190. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/mcp_/examples/retry_example.py +0 -0
  191. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/mcp_/health_monitor.py +0 -0
  192. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/mcp_/http_auth.py +0 -0
  193. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/mcp_/managed_server.py +0 -0
  194. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/mcp_/manager.py +0 -0
  195. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/mcp_/manager_lifecycle.py +0 -0
  196. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/mcp_/mcp_logs.py +0 -0
  197. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/mcp_/project_config.py +0 -0
  198. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/mcp_/registry.py +0 -0
  199. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/mcp_/retry_manager.py +0 -0
  200. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/mcp_/server_registry_catalog.py +0 -0
  201. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/mcp_/status_tracker.py +0 -0
  202. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/mcp_/system_tools.py +0 -0
  203. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/mcp_/tool_arg_coercion.py +0 -0
  204. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/mcp_/toolset_utils.py +0 -0
  205. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/mcp_prompts/__init__.py +0 -0
  206. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/mcp_prompts/hook_creator.py +0 -0
  207. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/__init__.py +0 -0
  208. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/bar_painters.py +0 -0
  209. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/bar_rendering.py +0 -0
  210. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/bottom_bar.py +0 -0
  211. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/bus.py +0 -0
  212. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/chords.py +0 -0
  213. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/commands.py +0 -0
  214. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/editor_actions.py +0 -0
  215. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/editor_completion.py +0 -0
  216. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/editor_display.py +0 -0
  217. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/editor_history.py +0 -0
  218. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/editor_keys.py +0 -0
  219. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/editor_paste.py +0 -0
  220. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/editor_queue.py +0 -0
  221. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/editor_submission.py +0 -0
  222. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/external_editor.py +0 -0
  223. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/identity_line.py +0 -0
  224. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/idle_status.py +0 -0
  225. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/inline_bar.py +0 -0
  226. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/line_editor.py +0 -0
  227. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/markdown_patches.py +0 -0
  228. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/menu_lifecycle.py +0 -0
  229. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/message_queue.py +0 -0
  230. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/messages.py +0 -0
  231. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/pause_controller.py +0 -0
  232. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/prompt_prefix_style.py +0 -0
  233. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/queue_console.py +0 -0
  234. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/renderers.py +0 -0
  235. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/rich_renderer.py +0 -0
  236. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/run_ui.py +0 -0
  237. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/run_ui_wiring.py +0 -0
  238. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/speculation_line.py +0 -0
  239. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/speculation_stats.py +0 -0
  240. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/speculation_toggle.py +0 -0
  241. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/spinner/__init__.py +0 -0
  242. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/status_line.py +0 -0
  243. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/subagent_console.py +0 -0
  244. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/theme_accent.py +0 -0
  245. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/tool_output.py +0 -0
  246. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/messaging/transcript_guard.py +0 -0
  247. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/model_descriptions.py +0 -0
  248. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/model_factory.py +0 -0
  249. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/model_utils.py +0 -0
  250. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/models.json +0 -0
  251. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/models_dev_api.json +0 -0
  252. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/models_dev_parser.py +0 -0
  253. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/observability.py +0 -0
  254. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/platform_utils.py +0 -0
  255. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/plugins/config.py +0 -0
  256. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/plugins/oauth_pasteback.py +0 -0
  257. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/plugins/oauth_puppy_html.py +0 -0
  258. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/plugins/trust.py +0 -0
  259. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/plugins/trust_notice.py +0 -0
  260. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/private_inference.py +0 -0
  261. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/provider_credentials.py +0 -0
  262. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/provider_identity.py +0 -0
  263. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/pydantic_patches.py +0 -0
  264. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/reopenable_async_client.py +0 -0
  265. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/round_robin_model.py +0 -0
  266. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/secret_store.py +0 -0
  267. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/secret_store_backends.py +0 -0
  268. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/session_format_migration.py +0 -0
  269. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/session_lifecycle.py +0 -0
  270. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/session_migration.py +0 -0
  271. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/session_storage.py +0 -0
  272. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/session_surrogate_unpickler.py +0 -0
  273. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/shared_credentials.py +0 -0
  274. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/skill_provider.py +0 -0
  275. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/splash.py +0 -0
  276. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/status_display.py +0 -0
  277. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/steer_metadata.py +0 -0
  278. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/terminal_utils.py +0 -0
  279. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/__init__.py +0 -0
  280. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/_browser_registry.py +0 -0
  281. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/_lazy.py +0 -0
  282. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/agent_tools.py +0 -0
  283. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/approval_feedback.py +0 -0
  284. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/ask_user_question/__init__.py +0 -0
  285. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/ask_user_question/constants.py +0 -0
  286. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/ask_user_question/demo_tui.py +0 -0
  287. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/ask_user_question/handler.py +0 -0
  288. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/ask_user_question/models.py +0 -0
  289. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/ask_user_question/registration.py +0 -0
  290. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/ask_user_question/renderers.py +0 -0
  291. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/ask_user_question/terminal_ui.py +0 -0
  292. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/ask_user_question/theme.py +0 -0
  293. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/ask_user_question/tui_loop.py +0 -0
  294. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/browser/__init__.py +0 -0
  295. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/browser/browser_control.py +0 -0
  296. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/browser/browser_interactions.py +0 -0
  297. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/browser/browser_locator_resolver.py +0 -0
  298. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/browser/browser_locators.py +0 -0
  299. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/browser/browser_manager.py +0 -0
  300. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/browser/browser_navigation.py +0 -0
  301. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/browser/browser_page_snapshot.py +0 -0
  302. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/browser/browser_screenshot.py +0 -0
  303. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/browser/browser_scripts.py +0 -0
  304. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/browser/browser_semantic_interactions.py +0 -0
  305. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/browser/browser_workflows.py +0 -0
  306. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/browser/tool_registry.py +0 -0
  307. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/command_runner.py +0 -0
  308. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/common.py +0 -0
  309. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/display.py +0 -0
  310. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/file_modifications.py +0 -0
  311. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/file_operations.py +0 -0
  312. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/file_permission_state.py +0 -0
  313. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/fs_access.py +0 -0
  314. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/image_tools.py +0 -0
  315. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/io_backends.py +0 -0
  316. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/model_tools.py +0 -0
  317. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/ripgrep.py +0 -0
  318. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/shell_backgrounding.py +0 -0
  319. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/skills_tools.py +0 -0
  320. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/subagent_context.py +0 -0
  321. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/subagent_usage_metrics.py +0 -0
  322. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/tools_content.py +0 -0
  323. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/universal_constructor.py +0 -0
  324. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/tools/win_shell_io.py +0 -0
  325. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/undo_manager.py +0 -0
  326. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/universal_constructor_provider.py +0 -0
  327. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/version_checker.py +0 -0
  328. {code_puppy-0.0.910 → code_puppy-0.0.911}/code_puppy/zai_model.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: code-puppy
3
- Version: 0.0.910
3
+ Version: 0.0.911
4
4
  Summary: Code generation agent
5
5
  Project-URL: repository, https://github.com/mpfaffenberger/code_puppy
6
6
  Project-URL: HomePage, https://github.com/mpfaffenberger/code_puppy
@@ -726,7 +726,7 @@ def build_pydantic_agent(
726
726
  history_compaction,
727
727
  ProcessHistory(steer_processor),
728
728
  build_response_clamp(),
729
- build_model_message_transform(logical_agent_name),
729
+ build_model_message_transform(logical_agent_name, agent),
730
730
  # Sub-agent recursion guards on the wrap_tool_execute seam
731
731
  # (denies invoke_agent calls past the depth caps before the
732
732
  # tool body runs). Sole wrap_tool_execute implementer, so
@@ -64,6 +64,7 @@ from code_puppy.config import (
64
64
  get_protected_token_count,
65
65
  get_summarization_model_name,
66
66
  )
67
+ from code_puppy.context_accounting import context_tokens, model_names
67
68
  from code_puppy.messaging import emit_warning
68
69
 
69
70
  # Kept importable at module level for the context-indicator plugin, which
@@ -202,7 +203,12 @@ class CodePuppyCompactionStore:
202
203
  return self.agent._get_model_context_length()
203
204
 
204
205
  def context_overhead(self) -> int:
205
- return self.agent._estimate_context_overhead()
206
+ overhead = self.agent._estimate_context_overhead()
207
+ # Cached so the outbound-request stamp (``_model_message_transform``)
208
+ # can report the exact overhead this measurement used, instead of
209
+ # re-deriving a possibly-different value moments later.
210
+ self.agent._context_overhead = overhead
211
+ return overhead
206
212
 
207
213
  def model_name(self) -> Optional[str]:
208
214
  if self.agent is None:
@@ -225,6 +231,17 @@ def _take_forced_compaction_request() -> bool:
225
231
  return get_pause_controller().take_compaction_request()
226
232
 
227
233
 
234
+ def _total_tokens(messages, model, overhead) -> int:
235
+ """Anchor the whole-history total to the live request model's identity.
236
+
237
+ Wired as the capability's ``total_estimator`` -- see
238
+ ``code_puppy.context_accounting.context_tokens`` for the anchor/fallback
239
+ logic. Kept as a module-level function (not a lambda) so it shows up by
240
+ name in tracebacks and can be monkeypatched directly in tests.
241
+ """
242
+ return context_tokens(messages, model_names(model), overhead)
243
+
244
+
228
245
  def build_history_compaction(agent: Any) -> PureHistoryCompaction:
229
246
  """Build the pure ``HistoryCompaction`` capability wired to Code Puppy.
230
247
 
@@ -243,6 +260,7 @@ def build_history_compaction(agent: Any) -> PureHistoryCompaction:
243
260
  strategy_name=lambda: get_compaction_strategy(),
244
261
  compaction_threshold=lambda: get_compaction_threshold(),
245
262
  token_estimator=estimate_tokens_for_message,
263
+ total_estimator=_total_tokens,
246
264
  message_hasher=hash_message,
247
265
  history_sanitizer=sanitize_tool_call_ids,
248
266
  force_poll=_take_forced_compaction_request,
@@ -0,0 +1,130 @@
1
+ """Plugin transforms for final outbound model messages."""
2
+
3
+ from copy import copy
4
+ from typing import Any
5
+
6
+ from pydantic_ai import RunContext
7
+ from pydantic_ai.capabilities import Hooks, WrapModelRequestHandler
8
+ from pydantic_ai.messages import ModelResponse
9
+ from pydantic_ai.models import ModelRequestContext
10
+ from pydantic_ai.models.wrapper import WrapperModel
11
+
12
+ from code_puppy.agents._foreign_thinking import strip_foreign_thinking
13
+ from code_puppy.callbacks import on_transform_model_messages
14
+
15
+ # Per-request prefix fingerprint, handed from the ``wrap_model_request`` leg
16
+ # to the ``after_model_request`` leg via this attribute on the (mutable,
17
+ # per-request) ``ModelRequestContext``. ``None`` means "this request's
18
+ # outbound messages diverged from the durable history it was built from, so
19
+ # don't stamp an anchor" -- see ``build_model_message_transform``.
20
+ _PREFIX_ATTR = "_context_accounting_prefix"
21
+
22
+
23
+ class _ContinuationObserver(WrapperModel):
24
+ """Observe a suspended->continued chain *before* pydantic-ai merges it.
25
+
26
+ ``continuation_delay`` runs on intermediate suspended responses before
27
+ their usage can merge. This request-local wrapper preserves the real
28
+ model's delay behavior while marking the final response ineligible for
29
+ a receipt. The framework's durable-execution capabilities use the same
30
+ request-model swap seam. Provider details and billing limitations live
31
+ in ``docs/API_CONTEXT_ACCOUNTING.md``.
32
+ """
33
+
34
+ def __init__(self, wrapped):
35
+ super().__init__(wrapped)
36
+ self.continued = False
37
+
38
+ def continuation_delay(self, response: ModelResponse) -> float | None:
39
+ self.continued = True
40
+ return super().continuation_delay(response)
41
+
42
+
43
+ def build_model_message_transform(agent_name: str | None, owner: Any = None) -> Hooks:
44
+ """Build the request-only plugin transform for an agent.
45
+
46
+ ``owner`` is the Code Puppy agent (or agent-shaped object) whose
47
+ ``_context_overhead`` compaction just computed for this same request --
48
+ see ``CodePuppyCompactionStore.context_overhead``. Passing it lets the
49
+ stamped receipt record the *exact* overhead the compaction trigger used,
50
+ so the next request's ``context_tokens`` call reconstructs the same
51
+ total instead of silently drifting by whatever overhead changed between
52
+ the two reads. ``None`` (sub-agents/tests that don't wire one) just
53
+ stamps ``overhead=0``, which only costs an extra estimate on that one
54
+ anchor -- never a wrong total, because ``context_tokens`` always folds
55
+ the caller's *current* overhead back in.
56
+ """
57
+
58
+ async def transform(
59
+ _ctx: RunContext[Any],
60
+ *,
61
+ request_context: ModelRequestContext,
62
+ handler: WrapModelRequestHandler,
63
+ ) -> ModelResponse:
64
+ from code_puppy.context_accounting import fingerprint, invalidate_anchors
65
+
66
+ durable_messages = request_context.messages
67
+ # Snapshot *before* anything can touch the messages. strip_foreign_thinking
68
+ # is a no-op pass-through for non-Anthropic models (same list AND same
69
+ # message objects by reference, not a copy) -- so a plugin transform
70
+ # that mutates in place would otherwise mutate ``durable_messages`` too,
71
+ # and a fingerprint taken afterwards would never see the divergence.
72
+ original_fingerprint = fingerprint(durable_messages)
73
+
74
+ transformed_context = copy(request_context)
75
+ transformed_context.messages = strip_foreign_thinking(
76
+ list(durable_messages), request_context.model
77
+ )
78
+ await on_transform_model_messages(agent_name, transformed_context.messages)
79
+
80
+ # The anchor must describe what the *durable history* looked like,
81
+ # because that's what next turn's ``context_tokens`` re-fingerprints
82
+ # against. If anything between the snapshot above and the handler
83
+ # call -- the built-in foreign-thinking strip, or a plugin's rewrite,
84
+ # in place or not -- changed what's actually sent, the response's
85
+ # usage no longer describes ``durable_messages`` and must not anchor it.
86
+ prefix = original_fingerprint
87
+ if fingerprint(transformed_context.messages) != prefix:
88
+ prefix = None
89
+
90
+ # Wrap *after* the fingerprint check above (which needs the real
91
+ # model for foreign-thinking detection) and *before* the handler
92
+ # call, so the continuation loop inside `handler` actually drives
93
+ # this wrapper's `continuation_delay` -- see `_ContinuationObserver`.
94
+ observer = _ContinuationObserver(transformed_context.model)
95
+ transformed_context.model = observer
96
+
97
+ try:
98
+ response = await handler(transformed_context)
99
+ except BaseException:
100
+ invalidate_anchors(durable_messages)
101
+ raise
102
+
103
+ if observer.continued:
104
+ # Usage on `response` is a cumulative sum across segments, not
105
+ # one measured prompt -- decline to anchor it. Billing/usage
106
+ # itself is untouched; only our own receipt is suppressed.
107
+ prefix = None
108
+ setattr(request_context, _PREFIX_ATTR, prefix)
109
+ return response
110
+
111
+ async def stamp(
112
+ _ctx: RunContext[Any],
113
+ *,
114
+ request_context: ModelRequestContext,
115
+ response: ModelResponse,
116
+ ) -> ModelResponse:
117
+ from code_puppy.context_accounting import model_names, record_anchor
118
+
119
+ prefix = getattr(request_context, _PREFIX_ATTR, None)
120
+ if prefix is not None:
121
+ record_anchor(
122
+ request_context.messages,
123
+ response,
124
+ context_overhead=getattr(owner, "_context_overhead", 0),
125
+ prefix=prefix,
126
+ request_model=model_names(request_context.model),
127
+ )
128
+ return response
129
+
130
+ return Hooks(model_request=transform, after_model_request=stamp)
@@ -24,7 +24,7 @@ from typing import Any, Callable, List, Optional, Protocol, Set, Tuple
24
24
  from pydantic_ai import CapabilityEvent
25
25
  from pydantic_ai.capabilities import AbstractCapability
26
26
  from pydantic_ai.messages import ModelMessage, ModelResponse, ThinkingPart
27
- from pydantic_ai.models import ModelRequestContext
27
+ from pydantic_ai.models import Model, ModelRequestContext
28
28
  from pydantic_ai.tools import RunContext
29
29
  from pydantic_ai.usage import RunUsage
30
30
 
@@ -255,6 +255,17 @@ class HistoryCompaction(AbstractCapability[Any]):
255
255
  message_hasher: Callable[[ModelMessage], str]
256
256
  """Stable content hash used for merge dedup + dropped tracking."""
257
257
 
258
+ total_estimator: Optional[Callable[[List[ModelMessage], Model, int], int]] = None
259
+ """Optional whole-history total, given the resolved request model.
260
+
261
+ When set, this replaces ``sum(token_estimator(...)) + context_overhead``
262
+ for both the pre- and post-compaction measurement. The seam exists so an
263
+ application can anchor the total to a provider-reported usage figure
264
+ instead of a per-message heuristic sum, without this module knowing
265
+ anything about how that anchor is produced or stored. ``None`` keeps the
266
+ historical sum-based behavior.
267
+ """
268
+
258
269
  history_sanitizer: Optional[Callable[[List[ModelMessage]], List[ModelMessage]]] = (
259
270
  None
260
271
  )
@@ -350,6 +361,24 @@ class HistoryCompaction(AbstractCapability[Any]):
350
361
  )
351
362
  return cleaned
352
363
 
364
+ def _total(
365
+ self,
366
+ ctx: RunContext[Any],
367
+ messages: List[ModelMessage],
368
+ model_name: Optional[str],
369
+ context_overhead: int,
370
+ ) -> int:
371
+ """Resolve the whole-history total via ``total_estimator`` if wired.
372
+
373
+ Falls back to the historical ``sum(token_estimator) + overhead`` when
374
+ no ``total_estimator`` was injected, so callers that never wire one
375
+ see unchanged behavior.
376
+ """
377
+ if self.total_estimator is not None:
378
+ return self.total_estimator(messages, ctx.model, context_overhead)
379
+ message_tokens = sum(self.token_estimator(m, model_name) for m in messages)
380
+ return message_tokens + context_overhead
381
+
353
382
  async def measure_and_compact(
354
383
  self,
355
384
  ctx: RunContext[Any],
@@ -366,8 +395,7 @@ class HistoryCompaction(AbstractCapability[Any]):
366
395
  always survive a failed compaction.
367
396
  """
368
397
  model_name = self.store.model_name()
369
- message_tokens = sum(self.token_estimator(m, model_name) for m in messages)
370
- total_tokens = message_tokens + context_overhead
398
+ total_tokens = self._total(ctx, messages, model_name, context_overhead)
371
399
  proportion_used = total_tokens / model_max if model_max else 0.0
372
400
 
373
401
  await _safe_emit(
@@ -425,8 +453,12 @@ class HistoryCompaction(AbstractCapability[Any]):
425
453
  dropped = [m for m in messages if self.message_hasher(m) not in result_hashes]
426
454
 
427
455
  # Parity with the historical implementation: the post-compaction
428
- # measurement reports message tokens only (no overhead term).
429
- final_token_count = sum(self.token_estimator(m, model_name) for m in result)
456
+ # measurement reports message tokens only (no overhead term). A
457
+ # ``total_estimator`` naturally falls back to the same "message
458
+ # tokens only" shape here too -- compaction just removed the
459
+ # earlier prefix an anchor would need, so the anchor fingerprint
460
+ # can never match and the estimator sums the (now-shorter) result.
461
+ final_token_count = self._total(ctx, result, model_name, 0)
430
462
  await _safe_emit(
431
463
  ctx,
432
464
  ContextUsageMeasuredEvent(
@@ -0,0 +1,154 @@
1
+ """Context totals anchored to completed API requests, with a heuristic delta.
2
+
3
+ A receipt identifies the unchanged measured prefix, response and request
4
+ model; new content and changed overhead remain estimates. Invalid receipts
5
+ fall back rather than reuse stale usage. Continuation-merged responses do
6
+ not anchor because cumulative billing is not a single measured prompt.
7
+
8
+ Design, provider limitations and rollout semantics are documented in
9
+ ``docs/API_CONTEXT_ACCOUNTING.md``. The request hook owns continuation
10
+ observation; this module never alters provider usage or billing.
11
+ """
12
+
13
+ import hashlib
14
+ import json
15
+
16
+ from pydantic_ai.messages import ModelMessagesTypeAdapter, ModelResponse, TextPart
17
+
18
+
19
+ def estimate_tokens_for_message(message, model_name):
20
+ """Import lazily so the shared entry point has no agent-package cycle."""
21
+ from code_puppy.agents._history import estimate_tokens_for_message as estimate
22
+
23
+ return estimate(message, model_name)
24
+
25
+
26
+ _ANCHOR = "context_anchor"
27
+
28
+
29
+ def fingerprint(messages) -> str:
30
+ """Include arguments, signatures and binary payloads, excluding receipts.
31
+
32
+ Receipts live under ``metadata`` so stamping one does not change the
33
+ fingerprint of the message it rides on -- otherwise a receipt would
34
+ invalidate itself the instant it was written.
35
+ """
36
+ clean = ModelMessagesTypeAdapter.dump_python(messages, mode="json")
37
+ payload = [
38
+ {
39
+ key: value
40
+ for key, value in message.items()
41
+ if key in {"parts", "instructions", "kind"}
42
+ }
43
+ for message in clean
44
+ ]
45
+ return hashlib.sha256(json.dumps(payload, sort_keys=True).encode()).hexdigest()
46
+
47
+
48
+ def record_anchor(
49
+ messages, response, *, context_overhead=0, prefix=None, request_model=None
50
+ ):
51
+ """Stamp only a successfully completed response; usage is never fabricated.
52
+
53
+ Trusts ``prefix`` and ``response.usage`` as given -- it has no way to
54
+ tell a continuation-merged (cumulative) usage from an ordinary one on
55
+ its own. The caller must not call this helper when a continuation was observed
56
+ (see ``_ContinuationObserver`` in ``agents/_model_message_transform.py``).
57
+ Here, ``prefix=None`` computes a fingerprint; it does not suppress stamping.
58
+ """
59
+ if (
60
+ response.usage.input_tokens <= 0
61
+ or not response.model_name
62
+ or response.state != "complete"
63
+ ):
64
+ return
65
+ response.metadata = dict(response.metadata or {})
66
+ response.metadata[_ANCHOR] = {
67
+ "prefix": prefix if prefix is not None else fingerprint(messages),
68
+ "response": fingerprint([response]),
69
+ "overhead": context_overhead,
70
+ "request_models": sorted(
71
+ request_model
72
+ if isinstance(request_model, frozenset)
73
+ else {request_model if request_model is not None else response.model_name}
74
+ ),
75
+ }
76
+
77
+
78
+ def invalidate_anchors(messages):
79
+ """Conservatively decline anchors after an unsuccessful request.
80
+
81
+ This is a deliberate failed-request policy, not proof the old measurement
82
+ became invalid. An unchanged prefix could retain its previous receipt,
83
+ and a retained partial response already forces fallback. We nevertheless
84
+ choose full estimation until a fresh completion; changing that contract
85
+ requires reconsidering the failure/cancellation regressions together.
86
+ """
87
+ for message in messages:
88
+ if isinstance(message, ModelResponse) and message.metadata:
89
+ message.metadata.pop(_ANCHOR, None)
90
+
91
+
92
+ def context_tokens(messages, model_name, context_overhead=0) -> int:
93
+ """One total for compaction, the status bar, and context metadata.
94
+
95
+ API input includes system/tool/cache tokens. Plain text output usage is
96
+ an incremental approximation; it may include hidden reasoning when no
97
+ ThinkingPart is exposed, conservatively overestimating replay. Visible
98
+ reasoning/tool replay is estimated instead.
99
+ Only the latest response may anchor: partial/zero usage must not cause a
100
+ silent rewind to an older observation.
101
+ """
102
+ names = model_name if isinstance(model_name, frozenset) else {model_name}
103
+ estimate_model = next(iter(names)) if len(names) == 1 else None
104
+ for index in range(len(messages) - 1, -1, -1):
105
+ response = messages[index]
106
+ if not isinstance(response, ModelResponse):
107
+ continue
108
+ receipt = (response.metadata or {}).get(_ANCHOR)
109
+ if (
110
+ isinstance(receipt, dict)
111
+ and response.usage.input_tokens > 0
112
+ and response.state == "complete"
113
+ and receipt.get("request_models") == sorted(names)
114
+ and receipt.get("prefix") == fingerprint(messages[:index])
115
+ and receipt.get("response") == fingerprint([response])
116
+ and isinstance(receipt.get("overhead"), (int, float))
117
+ ):
118
+ replay = (
119
+ response.usage.output_tokens
120
+ if response.parts
121
+ and all(isinstance(p, TextPart) for p in response.parts)
122
+ and response.usage.output_tokens > 0
123
+ else estimate_tokens_for_message(response, response.model_name)
124
+ )
125
+ return (
126
+ response.usage.input_tokens
127
+ + replay
128
+ + context_overhead
129
+ - receipt["overhead"]
130
+ + sum(
131
+ estimate_tokens_for_message(m, estimate_model)
132
+ for m in messages[index + 1 :]
133
+ )
134
+ )
135
+ break
136
+ return context_overhead + sum(
137
+ estimate_tokens_for_message(m, estimate_model) for m in messages
138
+ )
139
+
140
+
141
+ def active_model_name(agent):
142
+ """Use the request model identity, not an arbitrary config alias."""
143
+ model = getattr(agent, "cur_model", None)
144
+ if model is None:
145
+ model = getattr(getattr(agent, "pydantic_agent", None), "model", None)
146
+ return model_names(model) if model is not None else agent.get_model_name()
147
+
148
+
149
+ def model_names(model):
150
+ """Identify a request model together with its routing candidates."""
151
+ candidates = getattr(model, "models", ())
152
+ if candidates:
153
+ return frozenset({model.model_name, *(m.model_name for m in candidates)})
154
+ return model.model_name
@@ -85,8 +85,11 @@ def _refresh_context_status(agent) -> None:
85
85
  try:
86
86
  capacity = agent._get_model_context_length()
87
87
  history = agent.get_message_history() or []
88
- message_tokens = sum(agent.estimate_tokens_for_message(msg) for msg in history)
89
- total_tokens = message_tokens + agent._estimate_context_overhead()
88
+ from code_puppy.context_accounting import active_model_name, context_tokens
89
+
90
+ total_tokens = context_tokens(
91
+ history, active_model_name(agent), agent._estimate_context_overhead()
92
+ )
90
93
  proportion = total_tokens / capacity if capacity else 0.0
91
94
  update_spinner_context(format_context_info(total_tokens, capacity, proportion))
92
95
  except Exception:
@@ -190,6 +190,15 @@ def _load_installed_plugins() -> list[str]:
190
190
  )
191
191
  for entry_point in discovered:
192
192
  plugin_name = entry_point.name
193
+ if plugin_name == "token_ratio_learner":
194
+ # Retired: context accounting now anchors to the API's own
195
+ # reported prompt-token usage (see code_puppy.context_accounting)
196
+ # instead of a learned chars-per-token ratio. Older installed
197
+ # core-plugins bundles may still advertise this entry point for
198
+ # a while; skip it unconditionally rather than let a dead
199
+ # learner monkeypatch the shared fallback estimator.
200
+ logger.debug("Skipping retired plugin %s", plugin_name)
201
+ continue
193
202
  try:
194
203
  with _plugin_loading_context(plugin_name):
195
204
  entry_point.load()
@@ -3,36 +3,37 @@
3
3
  Kept separate from ``register_callbacks.py`` so this stays unit-testable in
4
4
  isolation and so callbacks file remains thin.
5
5
 
6
- Design note — *why* we re-implement the token counting locally:
7
-
8
- ``/context`` is supposed to be a *consistent*, model-agnostic view of how
9
- full the context window is. The core runtime has two layers that make
10
- token counts vary between models:
11
-
12
- 1. The ``token_ratio_learner`` plugin monkeypatches
13
- ``_history.estimate_tokens`` to use *learned* chars-per-token ratios
14
- per model. Great for compaction decisions — terrible for a
15
- user-facing dashboard, because the same conversation reports
16
- different token counts on different models.
17
-
18
- 2. ``_history._apply_multiplier`` bumps some models (e.g. Opus 4.7 by
19
- 1.35×) to compensate for tokenizers that over-tokenize relative to
20
- our heuristic. Again: useful for safety margins, lousy for
21
- "consistency between models".
22
-
23
- To keep ``/context`` honest and stable across model switches, this
24
- module uses its OWN raw ``max(1, floor(len(text) / 2.5))`` estimator
25
- and applies NO multiplier. Other parts of the system (compaction,
26
- summarization triggers) still use the calibrated values — that's
27
- intentional.
6
+ Design note — why the aggregate total is anchored, not independently estimated:
7
+
8
+ ``/context`` used to compute its own ``max(1, floor(len(text) / 2.5))``
9
+ estimate over the whole history, deliberately kept separate from
10
+ compaction's calibrated estimator so the two heuristics couldn't disagree
11
+ in confusing ways. But two heuristics measuring the same conversation can
12
+ still drift from each other *and* from what the provider actually billed
13
+ -- especially once caching is involved (Anthropic cache read/write, OpenAI
14
+ cached tokens) -- and neither was the ground truth.
15
+
16
+ ``code_puppy.context_accounting.context_tokens`` anchors the total to the
17
+ latest completed response's own API-reported prompt-token count and only
18
+ estimates the delta since then, falling back to a plain per-message
19
+ estimate when there's no valid anchor (first request, a rewritten prefix,
20
+ a model switch, or a failed/cancelled request). Compaction, the status bar,
21
+ and this module's aggregate total now share that one function, so they
22
+ can't disagree with each other -- and the number agrees with the account
23
+ you're actually billed on.
24
+
25
+ The per-bucket overhead breakdown below (system prompt, AGENTS.md, tool
26
+ schemas, MCP, kennel memory) stays a separate, deliberately raw heuristic:
27
+ it has no corresponding per-bucket figure in the API response to anchor
28
+ to, so it remains an estimate, not a billed-token partition.
28
29
  """
29
30
 
30
31
  from __future__ import annotations
31
32
 
32
33
  import json
33
34
  import math
34
- from dataclasses import dataclass
35
- from typing import Any, List, Optional
35
+ from dataclasses import dataclass, field
36
+ from typing import Any, List, Optional, cast
36
37
 
37
38
  __all__ = [
38
39
  "ContextUsage",
@@ -56,8 +57,8 @@ GREEN_CIRCLE = "🟢"
56
57
  YELLOW_CIRCLE = "🟡"
57
58
  RED_CIRCLE = "🔴"
58
59
 
59
- # Classic char/token heuristic, kept private so /context's numbers don't drift
60
- # if the core estimator is patched at runtime (token_ratio_learner plugin).
60
+ # Model-agnostic heuristic for approximate /context overhead buckets.
61
+ # Aggregate totals use shared accounting rather than these raw estimates.
61
62
  _CHARS_PER_TOKEN = 2.5
62
63
 
63
64
 
@@ -68,6 +69,18 @@ class ContextUsage:
68
69
  ``overhead_tokens`` is the *sum* of the per-bucket breakdown fields. The
69
70
  breakdown fields are optional (default 0) so legacy call sites that only
70
71
  care about the aggregate keep working without changes.
72
+
73
+ ``total_tokens`` is the authoritative aggregate -- the same number
74
+ compaction used to decide whether to trim. It is intentionally *not*
75
+ derived from ``used_tokens + overhead_tokens``: the overhead breakdown
76
+ is an independent, approximate re-estimate (e.g. it re-queries live MCP
77
+ tool schemas rather than the agent's cached server list), so it can be
78
+ larger *or* smaller than the shared total. Deriving the displayed total
79
+ from the breakdown would let an approximate bucket sum silently
80
+ override the number compaction actually decided against. Pass the real
81
+ total explicitly via the private ``_total_tokens`` constructor field;
82
+ when omitted (legacy/test call sites), it falls back to
83
+ ``used_tokens + overhead_tokens`` so existing behavior is unchanged.
71
84
  """
72
85
 
73
86
  used_tokens: int
@@ -79,10 +92,17 @@ class ContextUsage:
79
92
  pydantic_tools_tokens: int = 0
80
93
  mcp_tokens: int = 0
81
94
  kennel_memory_tokens: int = 0
95
+ _total_tokens: Optional[int] = field(default=None, repr=False)
96
+
97
+ def __post_init__(self) -> None:
98
+ if self._total_tokens is None:
99
+ object.__setattr__(
100
+ self, "_total_tokens", self.used_tokens + self.overhead_tokens
101
+ )
82
102
 
83
103
  @property
84
104
  def total_tokens(self) -> int:
85
- return self.used_tokens + self.overhead_tokens
105
+ return cast(int, self._total_tokens) # always set in __post_init__
86
106
 
87
107
  @property
88
108
  def proportion(self) -> float:
@@ -114,28 +134,15 @@ def pick_indicator(proportion: float) -> str:
114
134
  def _raw_estimate_tokens(text: str) -> int:
115
135
  """Pure char/2.5 heuristic. Identical for every model, every time.
116
136
 
117
- Mirrors the *original* ``_history.estimate_tokens`` before any plugin
118
- patches it. We deliberately don't import it — the whole point of this
119
- function is to stay immune to the token_ratio_learner monkeypatch.
137
+ Keeps detailed overhead buckets model-agnostic and independent of
138
+ calibrated message estimates. The authoritative aggregate is computed
139
+ separately through shared context accounting.
120
140
  """
121
141
  if not text:
122
142
  return 0
123
143
  return max(1, math.floor(len(text) / _CHARS_PER_TOKEN))
124
144
 
125
145
 
126
- def _raw_tokens_for_message(message: Any) -> int:
127
- """Sum raw tokens across a message's parts via the canonical stringifier."""
128
- # ``stringify_part`` is a pure formatter — safe to import directly.
129
- from code_puppy.agents._history import stringify_part
130
-
131
- total = 0
132
- for part in getattr(message, "parts", []) or []:
133
- part_str = stringify_part(part)
134
- if part_str:
135
- total += _raw_estimate_tokens(part_str)
136
- return total
137
-
138
-
139
146
  def _raw_tokens_for_pydantic_tools(tools: Optional[dict]) -> int:
140
147
  """Estimate tokens contributed by pydantic-ai registered tools.
141
148
 
@@ -370,11 +377,12 @@ def get_current_usage() -> Optional[ContextUsage]:
370
377
  Returns ``None`` whenever any required piece of data is unavailable —
371
378
  missing agent, missing model config, or *any* exception while estimating
372
379
  history/overhead/capacity. We deliberately do **not** fall back to
373
- zero on partial failures: a misleading 🟢 indicator is worse than no
380
+ zero on partial failures: a misleading green indicator is worse than no
374
381
  indicator at all (the prompt simply hides the badge).
375
382
 
376
- All token counts are computed with the raw model-agnostic heuristic
377
- so the badge stays stable when the user switches models mid-session.
383
+ Aggregate counting shares compaction's API anchor and fallback estimator
384
+ (see the module docstring). Detailed overhead buckets below are
385
+ estimates, not a billed-token partition.
378
386
  """
379
387
  try:
380
388
  from code_puppy.agents.agent_manager import get_current_agent
@@ -390,7 +398,6 @@ def get_current_usage() -> Optional[ContextUsage]:
390
398
 
391
399
  try:
392
400
  history = agent.get_message_history() or []
393
- used = sum(_raw_tokens_for_message(m) for m in history)
394
401
  capacity = agent._get_model_context_length()
395
402
  except Exception:
396
403
  return None
@@ -403,8 +410,16 @@ def get_current_usage() -> Optional[ContextUsage]:
403
410
  except Exception:
404
411
  return None
405
412
 
413
+ try:
414
+ from code_puppy.context_accounting import active_model_name, context_tokens
415
+
416
+ overhead = agent._estimate_context_overhead()
417
+ total = context_tokens(history, active_model_name(agent), overhead)
418
+ except Exception:
419
+ return None
420
+
406
421
  return ContextUsage(
407
- used_tokens=int(used),
422
+ used_tokens=max(0, int(total) - breakdown.total),
408
423
  overhead_tokens=breakdown.total,
409
424
  capacity=int(capacity),
410
425
  system_prompt_tokens=breakdown.system_prompt_tokens,
@@ -412,4 +427,8 @@ def get_current_usage() -> Optional[ContextUsage]:
412
427
  pydantic_tools_tokens=breakdown.pydantic_tools_tokens,
413
428
  mcp_tokens=breakdown.mcp_tokens,
414
429
  kennel_memory_tokens=breakdown.kennel_memory_tokens,
430
+ # Authoritative: must equal compaction's decision total exactly,
431
+ # even when the (approximate, independently-estimated) overhead
432
+ # breakdown is larger than it -- see ContextUsage's docstring.
433
+ _total_tokens=int(total),
415
434
  )
@@ -488,7 +488,7 @@ async def _invoke_agent_impl(
488
488
  # pydantic-ai v2).
489
489
  capabilities=[
490
490
  HistoryCompaction(agent_config),
491
- build_model_message_transform(agent_name),
491
+ build_model_message_transform(agent_name, agent_config),
492
492
  # Recursion guards ride the wrap_tool_execute seam so a
493
493
  # sub-agent's own invoke_agent calls are denied before
494
494
  # the tool body runs. Sole wrap_tool_execute implementer,