kolega-code 0.7.1__tar.gz → 0.7.2__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 (296) hide show
  1. {kolega_code-0.7.1 → kolega_code-0.7.2}/PKG-INFO +1 -1
  2. {kolega_code-0.7.1 → kolega_code-0.7.2}/RELEASING.md +6 -0
  3. {kolega_code-0.7.1 → kolega_code-0.7.2}/docs/src/content/docs/hooks.md +1 -0
  4. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/__init__.py +1 -1
  5. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/__init__.py +1 -1
  6. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/baseagent.py +62 -34
  7. kolega_code-0.7.2/kolega_code/agent/compression.py +146 -0
  8. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/conversation.py +131 -20
  9. kolega_code-0.7.2/kolega_code/agent/prompt_templates/auxiliary/compression/summary.system.md +28 -0
  10. kolega_code-0.7.2/kolega_code/agent/prompt_templates/auxiliary/compression/summary.user.md.j2 +17 -0
  11. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/prompts.py +7 -3
  12. kolega_code-0.7.2/kolega_code/agent/tests/compaction_helpers.py +189 -0
  13. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/test_base_agent.py +75 -68
  14. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/test_commands.py +51 -5
  15. kolega_code-0.7.2/kolega_code/agent/tests/test_compaction_integration.py +66 -0
  16. kolega_code-0.7.2/kolega_code/agent/tests/test_compaction_persistence.py +43 -0
  17. kolega_code-0.7.2/kolega_code/agent/tests/test_compaction_subagent.py +26 -0
  18. kolega_code-0.7.2/kolega_code/agent/tests/test_compaction_tui_events.py +98 -0
  19. kolega_code-0.7.2/kolega_code/agent/tests/test_compress_command.py +41 -0
  20. kolega_code-0.7.2/kolega_code/agent/tests/test_compression.py +143 -0
  21. kolega_code-0.7.2/kolega_code/agent/tests/test_conversation_compaction.py +192 -0
  22. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/test_prompts.py +1 -1
  23. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/utils/commands.py +26 -6
  24. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/cli/app.py +212 -21
  25. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/cli/main.py +3 -0
  26. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/cli/messages.py +2 -0
  27. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/cli/session_store.py +3 -0
  28. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/cli/tests/test_app.py +761 -0
  29. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/cli/tests/test_session_store.py +27 -0
  30. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/events.py +20 -0
  31. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/hooks/events.py +1 -0
  32. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/llm/specs.py +4 -4
  33. {kolega_code-0.7.1 → kolega_code-0.7.2}/pyproject.toml +1 -1
  34. {kolega_code-0.7.1 → kolega_code-0.7.2}/uv.lock +2 -2
  35. kolega_code-0.7.1/kolega_code/agent/compression.py +0 -81
  36. kolega_code-0.7.1/kolega_code/agent/prompt_templates/auxiliary/compression/summary.system.md +0 -59
  37. kolega_code-0.7.1/kolega_code/agent/prompt_templates/auxiliary/compression/summary.user.md.j2 +0 -18
  38. {kolega_code-0.7.1 → kolega_code-0.7.2}/.env.example +0 -0
  39. {kolega_code-0.7.1 → kolega_code-0.7.2}/.github/workflows/ci.yml +0 -0
  40. {kolega_code-0.7.1 → kolega_code-0.7.2}/.github/workflows/docs.yml +0 -0
  41. {kolega_code-0.7.1 → kolega_code-0.7.2}/.github/workflows/release.yml +0 -0
  42. {kolega_code-0.7.1 → kolega_code-0.7.2}/.gitignore +0 -0
  43. {kolega_code-0.7.1 → kolega_code-0.7.2}/CONTRIBUTING.md +0 -0
  44. {kolega_code-0.7.1 → kolega_code-0.7.2}/LICENSE +0 -0
  45. {kolega_code-0.7.1 → kolega_code-0.7.2}/README.md +0 -0
  46. {kolega_code-0.7.1 → kolega_code-0.7.2}/SECURITY.md +0 -0
  47. {kolega_code-0.7.1 → kolega_code-0.7.2}/conftest.py +0 -0
  48. {kolega_code-0.7.1 → kolega_code-0.7.2}/docs/.gitignore +0 -0
  49. {kolega_code-0.7.1 → kolega_code-0.7.2}/docs/README.md +0 -0
  50. {kolega_code-0.7.1 → kolega_code-0.7.2}/docs/astro.config.mjs +0 -0
  51. {kolega_code-0.7.1 → kolega_code-0.7.2}/docs/package-lock.json +0 -0
  52. {kolega_code-0.7.1 → kolega_code-0.7.2}/docs/package.json +0 -0
  53. {kolega_code-0.7.1 → kolega_code-0.7.2}/docs/public/favicon.svg +0 -0
  54. {kolega_code-0.7.1 → kolega_code-0.7.2}/docs/src/assets/demo.gif +0 -0
  55. {kolega_code-0.7.1 → kolega_code-0.7.2}/docs/src/assets/kolega-dark.svg +0 -0
  56. {kolega_code-0.7.1 → kolega_code-0.7.2}/docs/src/assets/kolega-light.svg +0 -0
  57. {kolega_code-0.7.1 → kolega_code-0.7.2}/docs/src/content/docs/cli/ask.md +0 -0
  58. {kolega_code-0.7.1 → kolega_code-0.7.2}/docs/src/content/docs/cli/doctor.md +0 -0
  59. {kolega_code-0.7.1 → kolega_code-0.7.2}/docs/src/content/docs/cli/overview.md +0 -0
  60. {kolega_code-0.7.1 → kolega_code-0.7.2}/docs/src/content/docs/cli/sessions.md +0 -0
  61. {kolega_code-0.7.1 → kolega_code-0.7.2}/docs/src/content/docs/concepts/agents.md +0 -0
  62. {kolega_code-0.7.1 → kolega_code-0.7.2}/docs/src/content/docs/concepts/how-it-works.md +0 -0
  63. {kolega_code-0.7.1 → kolega_code-0.7.2}/docs/src/content/docs/concepts/tools.md +0 -0
  64. {kolega_code-0.7.1 → kolega_code-0.7.2}/docs/src/content/docs/configuration/environment-variables.md +0 -0
  65. {kolega_code-0.7.1 → kolega_code-0.7.2}/docs/src/content/docs/configuration/providers-and-models.mdx +0 -0
  66. {kolega_code-0.7.1 → kolega_code-0.7.2}/docs/src/content/docs/configuration/settings-and-api-keys.mdx +0 -0
  67. {kolega_code-0.7.1 → kolega_code-0.7.2}/docs/src/content/docs/configuration/sign-in-with-chatgpt.mdx +0 -0
  68. {kolega_code-0.7.1 → kolega_code-0.7.2}/docs/src/content/docs/getting-started/installation.mdx +0 -0
  69. {kolega_code-0.7.1 → kolega_code-0.7.2}/docs/src/content/docs/getting-started/quick-start.mdx +0 -0
  70. {kolega_code-0.7.1 → kolega_code-0.7.2}/docs/src/content/docs/gigacode.mdx +0 -0
  71. {kolega_code-0.7.1 → kolega_code-0.7.2}/docs/src/content/docs/index.mdx +0 -0
  72. {kolega_code-0.7.1 → kolega_code-0.7.2}/docs/src/content/docs/skills.mdx +0 -0
  73. {kolega_code-0.7.1 → kolega_code-0.7.2}/docs/src/content/docs/tui/composer.md +0 -0
  74. {kolega_code-0.7.1 → kolega_code-0.7.2}/docs/src/content/docs/tui/interface.md +0 -0
  75. {kolega_code-0.7.1 → kolega_code-0.7.2}/docs/src/content/docs/tui/modes.md +0 -0
  76. {kolega_code-0.7.1 → kolega_code-0.7.2}/docs/src/content/docs/tui/sessions-and-resume.md +0 -0
  77. {kolega_code-0.7.1 → kolega_code-0.7.2}/docs/src/content/docs/tui/slash-commands.md +0 -0
  78. {kolega_code-0.7.1 → kolega_code-0.7.2}/docs/src/content.config.ts +0 -0
  79. {kolega_code-0.7.1 → kolega_code-0.7.2}/docs/src/styles/brand.css +0 -0
  80. {kolega_code-0.7.1 → kolega_code-0.7.2}/docs/tsconfig.json +0 -0
  81. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/browseragent.py +0 -0
  82. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/coder.py +0 -0
  83. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/common.py +0 -0
  84. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/context.py +0 -0
  85. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/generalagent.py +0 -0
  86. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/investigationagent.py +0 -0
  87. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/orchestration/__init__.py +0 -0
  88. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/orchestration/budget.py +0 -0
  89. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/orchestration/errors.py +0 -0
  90. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/orchestration/executor.py +0 -0
  91. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/orchestration/guide.py +0 -0
  92. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/orchestration/journal.py +0 -0
  93. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/orchestration/runtime.py +0 -0
  94. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/orchestration/tests/__init__.py +0 -0
  95. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/orchestration/tests/test_orchestration.py +0 -0
  96. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/orchestration/types.py +0 -0
  97. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/planningagent.py +0 -0
  98. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/prompt_provider.py +0 -0
  99. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/prompt_templates/auxiliary/terminal/output_summary.system.md +0 -0
  100. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/prompt_templates/auxiliary/terminal/safety.system.md +0 -0
  101. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/prompt_templates/auxiliary/tools/think_hard.system.md +0 -0
  102. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/prompt_templates/extensions/cli/planning_questions.md +0 -0
  103. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/prompt_templates/extensions/cli/shared_task_list.md +0 -0
  104. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/prompt_templates/extensions/skills/catalog.md.j2 +0 -0
  105. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/prompt_templates/system/agents/browser.md.j2 +0 -0
  106. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/prompt_templates/system/agents/coder_cli.md.j2 +0 -0
  107. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/prompt_templates/system/agents/general.md.j2 +0 -0
  108. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/prompt_templates/system/agents/investigation.md.j2 +0 -0
  109. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/prompt_templates/system/agents/planning.md.j2 +0 -0
  110. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/prompt_templates/system/includes/agents_md_instructions.md +0 -0
  111. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/prompt_templates/system/includes/environment_variables/workspace_env_vars.md +0 -0
  112. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/prompt_templates/system/includes/frontend_guidance.md +0 -0
  113. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/prompt_templates/system/includes/starter_templates/expo.md +0 -0
  114. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/prompt_templates/system/includes/starter_templates/html_website.md +0 -0
  115. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/prompt_templates/system/includes/starter_templates/mern_stack.md +0 -0
  116. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/prompt_templates/system/includes/starter_templates/react_vite_shadcdn.md +0 -0
  117. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/prompt_templates/user_tasks/cli/implement_plan.md.j2 +0 -0
  118. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/prompt_templates/user_tasks/cli/init_agents.md.j2 +0 -0
  119. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/__init__.py +0 -0
  120. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/llm/__init__.py +0 -0
  121. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/llm/test_anthropic_token_counting.py +0 -0
  122. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/llm/test_billing_openai_cache.py +0 -0
  123. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/llm/test_chatgpt_live.py +0 -0
  124. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/llm/test_chatgpt_oauth_provider.py +0 -0
  125. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/llm/test_client.py +0 -0
  126. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/llm/test_dashscope_mapping.py +0 -0
  127. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/llm/test_error_boundary.py +0 -0
  128. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/llm/test_exceptions.py +0 -0
  129. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/llm/test_google_thought_signature.py +0 -0
  130. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/llm/test_google_tool_signature_live.py +0 -0
  131. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/llm/test_instrumented_client.py +0 -0
  132. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/llm/test_instrumented_client_integration.py +0 -0
  133. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/llm/test_langfuse_normalization.py +0 -0
  134. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/llm/test_live_providers.py +0 -0
  135. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/llm/test_model_specs.py +0 -0
  136. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/llm/test_openai_cached_tokens.py +0 -0
  137. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/llm/test_openai_cached_tokens_stream.py +0 -0
  138. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/llm/test_openai_message_conversion.py +0 -0
  139. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/llm/test_openai_token_counting.py +0 -0
  140. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/llm/test_thinking_effort.py +0 -0
  141. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/llm/test_tool_execution_ids.py +0 -0
  142. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/services/__init__.py +0 -0
  143. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/services/test_browser.py +0 -0
  144. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/services/test_browser_parity.py +0 -0
  145. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/services/test_file_system.py +0 -0
  146. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/services/test_sandbox_terminal_input.py +0 -0
  147. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/services/test_terminal.py +0 -0
  148. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/services/test_terminal_buffer.py +0 -0
  149. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/services/test_terminal_state_serializer.py +0 -0
  150. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/test_agent_tools_inventory.py +0 -0
  151. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/test_coder_attachments.py +0 -0
  152. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/test_coder_prompt_extensions.py +0 -0
  153. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/test_duplicate_tool_results.py +0 -0
  154. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/test_empty_message_handling.py +0 -0
  155. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/test_event_emitter.py +0 -0
  156. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/test_general_agent.py +0 -0
  157. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/test_gigacode_gating.py +0 -0
  158. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/test_hooks_integration.py +0 -0
  159. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/test_html.py +0 -0
  160. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/test_parallel_tool_calls.py +0 -0
  161. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/test_permissions.py +0 -0
  162. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/test_planning_agent.py +0 -0
  163. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/test_prompt_provider.py +0 -0
  164. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/test_tool_registry.py +0 -0
  165. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/test_tools.py +0 -0
  166. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/tool_backend/__init__.py +0 -0
  167. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/tool_backend/test_agent_tool.py +0 -0
  168. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/tool_backend/test_base_tool.py +0 -0
  169. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/tool_backend/test_browser_tool.py +0 -0
  170. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/tool_backend/test_build_tool.py +0 -0
  171. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/tool_backend/test_create_file_tool.py +0 -0
  172. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/tool_backend/test_edit_preview.py +0 -0
  173. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/tool_backend/test_glob_tool.py +0 -0
  174. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/tool_backend/test_glob_tool_sandbox_parity.py +0 -0
  175. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/tool_backend/test_list_directory_tool.py +0 -0
  176. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/tool_backend/test_read_file_tool.py +0 -0
  177. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/tool_backend/test_replace_entire_file_tool.py +0 -0
  178. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/tool_backend/test_replace_lines_tool.py +0 -0
  179. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/tool_backend/test_search_and_replace_tool.py +0 -0
  180. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/tool_backend/test_search_codebase_tool.py +0 -0
  181. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/tool_backend/test_terminal_tool.py +0 -0
  182. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/tool_backend/test_think_hard_integration.py +0 -0
  183. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/tool_backend/test_think_hard_streaming.py +0 -0
  184. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/tool_backend/test_web_fetch_tool.py +0 -0
  185. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/tool_backend/test_web_search_tool.py +0 -0
  186. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tests/tool_backend/test_workflow_tool.py +0 -0
  187. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tool_backend/agent_tool.py +0 -0
  188. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tool_backend/base_tool.py +0 -0
  189. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tool_backend/browser_tool.py +0 -0
  190. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tool_backend/build_tool.py +0 -0
  191. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tool_backend/create_file_tool.py +0 -0
  192. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tool_backend/edit_preview.py +0 -0
  193. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tool_backend/glob_tool.py +0 -0
  194. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tool_backend/list_directory_tool.py +0 -0
  195. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tool_backend/memory_tool.py +0 -0
  196. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tool_backend/read_file_tool.py +0 -0
  197. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tool_backend/replace_entire_file_tool.py +0 -0
  198. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tool_backend/replace_lines_tool.py +0 -0
  199. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tool_backend/search_and_replace_tool.py +0 -0
  200. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tool_backend/search_backends/__init__.py +0 -0
  201. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tool_backend/search_backends/base.py +0 -0
  202. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tool_backend/search_backends/duckduckgo.py +0 -0
  203. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tool_backend/search_backends/errors.py +0 -0
  204. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tool_backend/search_backends/firecrawl.py +0 -0
  205. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tool_backend/search_backends/models.py +0 -0
  206. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tool_backend/search_backends/searxng.py +0 -0
  207. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tool_backend/search_backends/tavily.py +0 -0
  208. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tool_backend/search_codebase_tool.py +0 -0
  209. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tool_backend/streaming_tool.py +0 -0
  210. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tool_backend/terminal_tool.py +0 -0
  211. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tool_backend/think_hard_tool.py +0 -0
  212. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tool_backend/web_fetch_tool.py +0 -0
  213. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tool_backend/web_search_tool.py +0 -0
  214. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tool_backend/workflow_tool.py +0 -0
  215. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/agent/tools.py +0 -0
  216. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/auth/__init__.py +0 -0
  217. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/auth/_jwt.py +0 -0
  218. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/auth/chatgpt_oauth.py +0 -0
  219. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/auth/constants.py +0 -0
  220. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/auth/tests/__init__.py +0 -0
  221. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/auth/tests/test_chatgpt_oauth.py +0 -0
  222. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/auth/tests/test_tokens.py +0 -0
  223. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/auth/tokens.py +0 -0
  224. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/cli/__init__.py +0 -0
  225. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/cli/config.py +0 -0
  226. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/cli/connection.py +0 -0
  227. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/cli/file_index.py +0 -0
  228. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/cli/mentions.py +0 -0
  229. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/cli/provider_registry.py +0 -0
  230. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/cli/settings.py +0 -0
  231. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/cli/skills.py +0 -0
  232. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/cli/slash_commands.py +0 -0
  233. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/cli/tests/__init__.py +0 -0
  234. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/cli/tests/test_cli_config.py +0 -0
  235. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/cli/tests/test_connection.py +0 -0
  236. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/cli/tests/test_file_index.py +0 -0
  237. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/cli/tests/test_main.py +0 -0
  238. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/cli/tests/test_mentions.py +0 -0
  239. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/cli/tests/test_settings.py +0 -0
  240. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/cli/tests/test_skills.py +0 -0
  241. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/cli/tests/test_slash_commands.py +0 -0
  242. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/cli/tests/test_themes.py +0 -0
  243. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/cli/tests/test_updater.py +0 -0
  244. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/cli/theme.py +0 -0
  245. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/cli/updater.py +0 -0
  246. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/config.py +0 -0
  247. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/hooks/__init__.py +0 -0
  248. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/hooks/backends.py +0 -0
  249. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/hooks/config.py +0 -0
  250. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/hooks/dispatcher.py +0 -0
  251. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/hooks/matcher.py +0 -0
  252. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/hooks/outcome.py +0 -0
  253. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/hooks/tests/__init__.py +0 -0
  254. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/hooks/tests/test_hooks.py +0 -0
  255. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/llm/client.py +0 -0
  256. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/llm/exceptions.py +0 -0
  257. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/llm/instrumented_client.py +0 -0
  258. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/llm/models.py +0 -0
  259. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/llm/providers/__init__.py +0 -0
  260. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/llm/providers/anthropic.py +0 -0
  261. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/llm/providers/base.py +0 -0
  262. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/llm/providers/chatgpt_oauth.py +0 -0
  263. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/llm/providers/google.py +0 -0
  264. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/llm/providers/models.py +0 -0
  265. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/llm/providers/openai.py +0 -0
  266. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/llm/ratelimit.py +0 -0
  267. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/llm/tool_execution_ids.py +0 -0
  268. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/models/__init__.py +0 -0
  269. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/models/sandbox_terminal_state.py +0 -0
  270. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/permissions.py +0 -0
  271. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/runtime.py +0 -0
  272. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/sandbox/README.md +0 -0
  273. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/sandbox/__init__.py +0 -0
  274. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/sandbox/async_filesystem.py +0 -0
  275. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/sandbox/base.py +0 -0
  276. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/sandbox/browser.py +0 -0
  277. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/sandbox/event_loop.py +0 -0
  278. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/sandbox/filesystem.py +0 -0
  279. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/sandbox/local.py +0 -0
  280. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/sandbox/serializer.py +0 -0
  281. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/sandbox/terminal.py +0 -0
  282. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/sandbox/utils.py +0 -0
  283. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/services/base.py +0 -0
  284. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/services/browser.py +0 -0
  285. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/services/file_system.py +0 -0
  286. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/services/html.py +0 -0
  287. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/services/terminal.py +0 -0
  288. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/services/terminal_buffer.py +0 -0
  289. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/tools/__init__.py +0 -0
  290. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/tools/core.py +0 -0
  291. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/tools/definitions.py +0 -0
  292. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/tools/registry.py +0 -0
  293. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/tools/tests/__init__.py +0 -0
  294. {kolega_code-0.7.1 → kolega_code-0.7.2}/kolega_code/tools/tests/test_definitions.py +0 -0
  295. {kolega_code-0.7.1 → kolega_code-0.7.2}/run_tests.sh +0 -0
  296. {kolega_code-0.7.1 → kolega_code-0.7.2}/scripts/install-kolega-code.sh +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: kolega-code
3
- Version: 0.7.1
3
+ Version: 0.7.2
4
4
  Summary: Local-first AI coding agent for the terminal
5
5
  Project-URL: Homepage, https://kolega-ai.github.io/kolega-code/
6
6
  Project-URL: Documentation, https://kolega-ai.github.io/kolega-code/
@@ -20,6 +20,12 @@ release branch.
20
20
  2. Update `pyproject.toml`, `uv.lock`, and package `__version__` values to the
21
21
  release version.
22
22
 
23
+ Running `uv lock` advances the `exclude-newer` cutoff recorded in `uv.lock`,
24
+ because `pyproject.toml` sets `exclude-newer = "1 week"` as a relative
25
+ duration. The cutoff timestamp moving forward, and any dependency versions
26
+ released within the new one-week window being pulled in, is expected and
27
+ acceptable. Do not revert these changes.
28
+
23
29
  3. Run the fast test suite:
24
30
 
25
31
  ```bash
@@ -63,6 +63,7 @@ Both files use the same shape:
63
63
  | `PreToolUse` | Before a tool runs (after the permission gate) | Denies the tool; the reason is returned to the agent as a tool error, so it can adjust |
64
64
  | `PostToolUse` | After a tool succeeds (and on failure) | Ends the turn; the reason is shown as a warning |
65
65
  | `PreCompact` | Before the conversation is compacted | Advisory |
66
+ | `PostCompact` | After the conversation is compacted | Advisory; payload reports before/after token counts |
66
67
  | `Stop` | The agent is about to finish its turn | Keeps the agent working; the reason becomes its next instruction |
67
68
  | `SubagentStop` | A dispatched sub-agent finished | Advisory; can annotate the result the parent sees |
68
69
  | `Notification` | A permission prompt is about to be shown | Advisory (desktop notifications, sounds) |
@@ -148,4 +148,4 @@ __all__ = [
148
148
  "parse_git_status_output",
149
149
  ]
150
150
 
151
- __version__ = "0.7.1"
151
+ __version__ = "0.7.2"
@@ -39,4 +39,4 @@ __all__ = [
39
39
  "ToolExtension",
40
40
  ]
41
41
 
42
- __version__ = "0.7.1"
42
+ __version__ = "0.7.2"
@@ -11,7 +11,7 @@ from pathlib import Path
11
11
  from typing import Any, AsyncGenerator, Dict, List, Optional
12
12
 
13
13
  from .common import LogMixin
14
- from .compression import HistoryCompressor
14
+ from .compression import CompactionResult, HistoryCompressor
15
15
  from kolega_code.config import AgentConfig, ModelProvider
16
16
  from kolega_code.events import AgentConnectionManager
17
17
  from .context import AgentContext, AgentServices, Telemetry, WorkspaceInfo
@@ -398,6 +398,14 @@ class BaseAgent(LogMixin):
398
398
  """Restores the message history from a list of dictionaries using custom methods."""
399
399
  self.conversation.restore(serialized_history)
400
400
 
401
+ def dump_compaction_state(self) -> Dict[str, Any]:
402
+ """Serialize the compaction boundary (summary + how many leading messages it folds)."""
403
+ return self.conversation.dump_compaction()
404
+
405
+ def restore_compaction_state(self, data: Optional[Dict[str, Any]]) -> None:
406
+ """Restore the compaction boundary; must be called after restore_message_history."""
407
+ self.conversation.restore_compaction(data)
408
+
401
409
  def _sanitize_oversized_tool_results(self) -> int:
402
410
  return self.conversation.sanitize_oversized_tool_results()
403
411
 
@@ -549,9 +557,12 @@ class BaseAgent(LogMixin):
549
557
  message=message,
550
558
  )
551
559
 
552
- async def compress_history(self) -> None:
560
+ async def compress_history(self) -> CompactionResult:
553
561
  """
554
- Non-destructively summarize the current history and mark a compression boundary.
562
+ Non-destructively summarize the current history, keeping recent turns
563
+ verbatim. Emits a compaction_status event around the work so the UI can
564
+ show progress, then recounts + emits a context_update so the gauge
565
+ refreshes. Returns the structured outcome.
555
566
  """
556
567
 
557
568
  async def on_info(message: str) -> None:
@@ -560,16 +571,34 @@ class BaseAgent(LogMixin):
560
571
  async def on_error(message: str) -> None:
561
572
  await self.log_error(message, sender=self.agent_name)
562
573
 
563
- await self.compressor.summarize(
564
- self.conversation,
565
- llm=self.llm,
566
- model=self.primary_model_config.model,
567
- max_completion_tokens=self.model_completion_tokens,
568
- temperature=self.model_default_temperature,
569
- thinking=self.primary_model_config.thinking_effort,
570
- on_info=on_info,
571
- on_error=on_error,
574
+ await self.emitter.compaction_status("started", "Compacting conversation…")
575
+ try:
576
+ result = await self.compressor.summarize(
577
+ self.conversation,
578
+ llm=self.llm,
579
+ model=self.primary_model_config.model,
580
+ temperature=self.model_default_temperature,
581
+ # No extended thinking: a bounded summary doesn't need it, and the
582
+ # thinking budget could otherwise exceed the small summary max_tokens.
583
+ thinking=None,
584
+ on_info=on_info,
585
+ on_error=on_error,
586
+ )
587
+ finally:
588
+ # Recount + emit so the context gauge reflects post-compaction reality
589
+ # (even on a no-op the UI may have been stale).
590
+ await self.count_current_context()
591
+
592
+ phase = "finished" if result.ok else "error"
593
+ summary_text = (
594
+ self.conversation.summary.get_text_content() if result.ok and self.conversation.summary else ""
572
595
  )
596
+ await self.emitter.compaction_status(phase, result.message, summary=summary_text)
597
+ return result
598
+
599
+ def clear_history(self) -> None:
600
+ """Drop all history and reset compaction state."""
601
+ self.conversation.clear()
573
602
 
574
603
  # ------------------------------------------------------------------
575
604
  # Tool execution
@@ -1136,9 +1165,8 @@ class BaseAgent(LogMixin):
1136
1165
  #
1137
1166
  # process_message_stream is the single canonical loop shared by every
1138
1167
  # agent. Subclasses customize behavior through the hook methods below
1139
- # (build_user_content, apply_compression_fallback, on_tool_use_start,
1140
- # should_stop_after_tools, recap_agent_outcome) rather than overriding
1141
- # the loop itself.
1168
+ # (build_user_content, on_tool_use_start, should_stop_after_tools,
1169
+ # recap_agent_outcome) rather than overriding the loop itself.
1142
1170
  # ------------------------------------------------------------------
1143
1171
 
1144
1172
  completion_log_message = "Processing complete"
@@ -1166,21 +1194,6 @@ class BaseAgent(LogMixin):
1166
1194
 
1167
1195
  return content_blocks
1168
1196
 
1169
- def apply_compression_fallback(self) -> None:
1170
- """
1171
- Hard-truncate history when compression alone could not get under budget.
1172
-
1173
- Default: keep the first message (the original task) plus any protected
1174
- skill-content messages.
1175
- """
1176
- first_message = self.history[0]
1177
- protected = [
1178
- message
1179
- for message in self.history
1180
- if message is not first_message and self._is_protected_skill_content(message)
1181
- ]
1182
- self.history = MessageHistory(protected + [first_message])
1183
-
1184
1197
  async def on_tool_use_start(self, tool_call_delta: Dict[str, Any]) -> None:
1185
1198
  """
1186
1199
  Called when the provider streams a tool_use_start event (Anthropic only).
@@ -1252,20 +1265,35 @@ class BaseAgent(LogMixin):
1252
1265
  logger.debug("Input token count: %s", token_count)
1253
1266
 
1254
1267
  if self.compressor.over_budget(token_count.input_tokens, self.model_context_length):
1268
+ before_tokens = token_count.input_tokens
1255
1269
  # PreCompact hooks (advisory): observe before history is compacted.
1256
1270
  await self.fire_hook(
1257
1271
  HookEvent.PRE_COMPACT,
1258
1272
  {
1259
1273
  "trigger": "auto",
1260
- "input_tokens": token_count.input_tokens,
1274
+ "input_tokens": before_tokens,
1261
1275
  "model_context_length": self.model_context_length,
1262
1276
  },
1263
1277
  )
1264
- await self.compress_history()
1278
+ result = await self.compress_history()
1265
1279
  token_count = await self.count_current_context()
1266
1280
 
1267
- if self.compressor.over_budget(token_count.input_tokens, self.model_context_length):
1268
- self.apply_compression_fallback()
1281
+ # PostCompact hooks (advisory): observe the outcome. There is no
1282
+ # destructive fallback — a bounded summary plus capped tool results
1283
+ # and a small verbatim tail keep us under budget; if somehow not, we
1284
+ # send as-is rather than wipe history.
1285
+ await self.fire_hook(
1286
+ HookEvent.POST_COMPACT,
1287
+ {
1288
+ "trigger": "auto",
1289
+ "ok": result.ok,
1290
+ "reason": result.reason,
1291
+ "summarized_messages": result.summarized_messages,
1292
+ "input_tokens_before": before_tokens,
1293
+ "input_tokens_after": token_count.input_tokens,
1294
+ "model_context_length": self.model_context_length,
1295
+ },
1296
+ )
1269
1297
 
1270
1298
  self.mark_cache_checkpoint()
1271
1299
 
@@ -0,0 +1,146 @@
1
+ """History compression: summarize a conversation when it outgrows the context window."""
2
+
3
+ import logging
4
+ from dataclasses import dataclass
5
+ from typing import Awaitable, Callable, Optional
6
+
7
+ from .conversation import Conversation
8
+ from kolega_code.llm.models import Message, MessageHistory, TextBlock
9
+ from .prompts import (
10
+ build_compression_summary_user_prompt,
11
+ COMPRESSION_SUMMARY_SYSTEM_PROMPT,
12
+ )
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+ LogCallback = Callable[[str], Awaitable[None]]
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class CompactionResult:
21
+ """Outcome of a compaction attempt, surfaced to callers and the UI.
22
+
23
+ ``reason`` is a machine tag: "ok" | "too_few" | "nothing_to_summarize" | "llm_error".
24
+ ``message`` is a human-readable line for the command output / logs.
25
+ """
26
+
27
+ ok: bool
28
+ reason: str
29
+ summarized_messages: int = 0
30
+ message: str = ""
31
+
32
+
33
+ class HistoryCompressor:
34
+ """Summarizes a conversation non-destructively when it crosses the budget threshold."""
35
+
36
+ MIN_MESSAGES_TO_COMPRESS = 5
37
+ # How many of the most recent messages to keep verbatim after the summary.
38
+ KEEP_RECENT_MESSAGES = 6
39
+ # Don't bother summarizing a trivially short prefix.
40
+ MIN_PREFIX_TO_SUMMARIZE = 3
41
+ # Cap the summary length: the prompt targets ~600 words (~900 tokens), so a
42
+ # small ceiling keeps it tight and avoids the model's full completion budget.
43
+ SUMMARY_MAX_TOKENS = 2048
44
+
45
+ def __init__(self, threshold: float = 0.8) -> None:
46
+ # Fraction of the model context window above which compression kicks in
47
+ self.threshold = threshold
48
+
49
+ def over_budget(self, input_tokens: int, model_context_length: int) -> bool:
50
+ return input_tokens > model_context_length * self.threshold
51
+
52
+ async def summarize(
53
+ self,
54
+ conversation: Conversation,
55
+ *,
56
+ llm,
57
+ model: str,
58
+ temperature: float,
59
+ thinking,
60
+ on_info: Optional[LogCallback] = None,
61
+ on_error: Optional[LogCallback] = None,
62
+ ) -> CompactionResult:
63
+ """
64
+ Non-destructively summarize the aged-out prefix while keeping the most
65
+ recent turns verbatim, and move the compaction boundary.
66
+
67
+ Incremental: only the messages that have aged out since the previous
68
+ boundary are summarized, with the prior summary folded in for continuity.
69
+ Returns a CompactionResult describing what happened — never a silent no-op.
70
+ """
71
+ history = conversation.history
72
+ if not history or len(history) < self.MIN_MESSAGES_TO_COMPRESS:
73
+ return CompactionResult(
74
+ ok=False,
75
+ reason="too_few",
76
+ message=(
77
+ f"Nothing to compress yet ({len(history)} message(s); "
78
+ f"need at least {self.MIN_MESSAGES_TO_COMPRESS})."
79
+ ),
80
+ )
81
+
82
+ split = conversation.compaction_split_point(
83
+ keep_recent=self.KEEP_RECENT_MESSAGES, min_prefix=self.MIN_PREFIX_TO_SUMMARIZE
84
+ )
85
+ prior_through = conversation.compacted_through if conversation.summary is not None else 0
86
+ if split is None or split <= prior_through:
87
+ return CompactionResult(
88
+ ok=False,
89
+ reason="nothing_to_summarize",
90
+ message="Already compact — no older messages to summarize.",
91
+ )
92
+
93
+ if on_info:
94
+ await on_info("Compressing message history...")
95
+
96
+ try:
97
+ prefix = MessageHistory(list(history[prior_through:split]))
98
+ prefix_markdown = prefix.get_markdown_conversation()
99
+ previous_summary = conversation.summary.get_text_content() if conversation.summary is not None else None
100
+ user_prompt_filled = build_compression_summary_user_prompt(
101
+ prefix_markdown, previous_summary=previous_summary
102
+ )
103
+
104
+ messages = MessageHistory([Message(role="user", content=[TextBlock(text=user_prompt_filled)])])
105
+ system_message = Message(role="system", content=[TextBlock(text=COMPRESSION_SUMMARY_SYSTEM_PROMPT)])
106
+
107
+ # Stream and drain rather than calling generate(): the Anthropic SDK
108
+ # rejects non-streaming requests whose max_tokens is large enough to risk
109
+ # a >10-minute response, which the model's full completion budget triggers.
110
+ async with await llm.stream(
111
+ messages=messages,
112
+ system=system_message,
113
+ temperature=temperature,
114
+ model=model,
115
+ max_completion_tokens=self.SUMMARY_MAX_TOKENS,
116
+ thinking=thinking,
117
+ ) as stream:
118
+ async for _event in stream:
119
+ pass
120
+ response = await stream.get_final_message()
121
+
122
+ summary_text = response.get_text_content()
123
+ if not summary_text or not summary_text.strip():
124
+ msg = "Compression produced an empty summary; history left unchanged."
125
+ if on_error:
126
+ await on_error(msg)
127
+ else:
128
+ logger.error(msg)
129
+ return CompactionResult(ok=False, reason="llm_error", message=msg)
130
+
131
+ folded = split - prior_through
132
+ conversation.apply_compaction(summary_text, split)
133
+
134
+ done = f"Compressed {folded} older message(s) into a summary; kept the latest turns verbatim."
135
+ if on_info:
136
+ await on_info(done)
137
+ return CompactionResult(ok=True, reason="ok", summarized_messages=folded, message=done)
138
+
139
+ except Exception as e:
140
+ # Never swallow into a fake success: log the full traceback and surface
141
+ # the real error to the caller.
142
+ logger.exception("Failed to compress message history")
143
+ msg = f"Failed to compress message history: {type(e).__name__}: {e}"
144
+ if on_error:
145
+ await on_error(msg)
146
+ return CompactionResult(ok=False, reason="llm_error", message=msg)
@@ -27,11 +27,54 @@ class Conversation:
27
27
  *,
28
28
  max_tool_result_chars: int = 100_000,
29
29
  ) -> None:
30
- self.history = MessageHistory(list(messages) if messages else [])
31
- # Compression marker: index of the last message before a summary was appended
32
- self.last_compression_index: Optional[int] = None
30
+ self._history = MessageHistory(list(messages) if messages else [])
31
+ # Compaction state: ``summary`` stands in for ``history[:compacted_through]``
32
+ # in the effective view; messages from ``compacted_through`` onward are kept
33
+ # verbatim. ``summary is None`` / ``compacted_through == 0`` means uncompacted.
34
+ self.summary: Optional[Message] = None
35
+ self.compacted_through: int = 0
33
36
  self.max_tool_result_chars = max_tool_result_chars
34
37
 
38
+ # ------------------------------------------------------------------
39
+ # History access (compaction-aware)
40
+ # ------------------------------------------------------------------
41
+
42
+ @property
43
+ def history(self) -> MessageHistory:
44
+ return self._history
45
+
46
+ @history.setter
47
+ def history(self, value) -> None:
48
+ # Wholesale replacement of the log invalidates any compaction boundary.
49
+ # Internal mutators that must preserve an active summary assign to
50
+ # ``self._history`` directly instead of going through this setter.
51
+ self._history = value if isinstance(value, MessageHistory) else MessageHistory(list(value))
52
+ self.summary = None
53
+ self.compacted_through = 0
54
+
55
+ @property
56
+ def last_compression_index(self) -> Optional[int]:
57
+ """Back-compat: index of the last message folded into the summary, or None."""
58
+ if self.summary is not None and self.compacted_through > 0:
59
+ return self.compacted_through - 1
60
+ return None
61
+
62
+ @last_compression_index.setter
63
+ def last_compression_index(self, value: Optional[int]) -> None:
64
+ # Legacy setter. The only meaningful operation is clearing the boundary;
65
+ # a real summary is recorded through ``apply_compaction``/``record_compression``.
66
+ if value is None:
67
+ self.summary = None
68
+ self.compacted_through = 0
69
+ else:
70
+ self.compacted_through = value + 1
71
+
72
+ def clear(self) -> None:
73
+ """Drop all history and reset compaction state."""
74
+ self._history = MessageHistory([])
75
+ self.summary = None
76
+ self.compacted_through = 0
77
+
35
78
  # ------------------------------------------------------------------
36
79
  # Appending
37
80
  # ------------------------------------------------------------------
@@ -125,7 +168,9 @@ class Conversation:
125
168
  def extend(self, messages: List[Message]) -> None:
126
169
  """Extend history with multiple messages, repairing incomplete tool calls in the result."""
127
170
  all_messages = list(self.history) + messages
128
- self.history = MessageHistory(self.repaired(all_messages))
171
+ # Assign to _history (not the public setter) so an active compaction survives
172
+ # appending live turns.
173
+ self._history = MessageHistory(self.repaired(all_messages))
129
174
 
130
175
  # ------------------------------------------------------------------
131
176
  # Views and validity
@@ -134,20 +179,17 @@ class Conversation:
134
179
  def effective_history(self) -> MessageHistory:
135
180
  """
136
181
  Return the subset of history to send to the LLM:
137
- - If compressed: protected skill content + summary + all messages after the summary
138
- - Else: the full history
182
+ - If compacted: protected skill content from the folded prefix, then the
183
+ summary, then the most-recent turns kept verbatim.
184
+ - Else: the full history.
139
185
  """
140
- if self.last_compression_index is not None and self.history:
141
- # Summary is the message immediately after the boundary
142
- summary_idx = self.last_compression_index + 1
143
- if summary_idx < len(self.history):
144
- summary_msg = self.history[summary_idx]
145
- protected = [message for message in self.history[:summary_idx] if self.is_protected(message)]
146
- # Tail starts after the summary
147
- tail = list(self.history[summary_idx + 1 :]) if summary_idx + 1 < len(self.history) else []
148
- return MessageHistory(protected + [summary_msg] + tail)
149
-
150
- return MessageHistory(list(self.history))
186
+ if self.summary is not None and self.compacted_through > 0 and self._history:
187
+ cut = min(self.compacted_through, len(self._history))
188
+ protected = [message for message in self._history[:cut] if self.is_protected(message)]
189
+ tail = list(self._history[cut:])
190
+ return MessageHistory(protected + [self.summary] + tail)
191
+
192
+ return MessageHistory(list(self._history))
151
193
 
152
194
  def is_protected(self, message: Message) -> bool:
153
195
  """True for user messages carrying skill content that must survive compression."""
@@ -388,9 +430,55 @@ class Conversation:
388
430
  return sanitized_count
389
431
 
390
432
  def record_compression(self, summary: Message) -> None:
391
- """Append a compression summary and mark the boundary before it."""
392
- self.history.append(summary)
393
- self.last_compression_index = len(self.history) - 2
433
+ """Back-compat shim: fold the entire current history into ``summary``.
434
+
435
+ Newer callers use ``apply_compaction`` with an explicit split point so the
436
+ most recent turns are kept verbatim.
437
+ """
438
+ self.summary = (
439
+ summary if isinstance(summary, Message) else Message(role="user", content=[TextBlock(text=str(summary))])
440
+ )
441
+ self.compacted_through = len(self._history)
442
+
443
+ def apply_compaction(self, summary_text: str, split_point: int) -> None:
444
+ """Record ``summary_text`` as standing in for ``history[:split_point]``.
445
+
446
+ Non-destructive: the full history is untouched; only the compaction
447
+ boundary moves. Messages from ``split_point`` onward stay verbatim.
448
+ """
449
+ self.summary = Message(role="user", content=[TextBlock(text=summary_text)])
450
+ self.compacted_through = max(0, min(split_point, len(self._history)))
451
+
452
+ def compaction_split_point(self, *, keep_recent: int, min_prefix: int) -> Optional[int]:
453
+ """Index where the verbatim recent tail should begin.
454
+
455
+ Keeps the last ``keep_recent`` messages verbatim, then snaps the cut
456
+ backward so it never lands between an assistant tool_use and its
457
+ tool_result. Returns None when the prefix to summarize would be smaller
458
+ than ``min_prefix``.
459
+ """
460
+ n = len(self._history)
461
+ if n <= min_prefix:
462
+ return None
463
+ candidate = self._snap_to_safe_boundary(max(0, n - keep_recent))
464
+ if candidate < min_prefix:
465
+ return None
466
+ return candidate
467
+
468
+ def _snap_to_safe_boundary(self, idx: int) -> int:
469
+ """Move ``idx`` backward past any assistant tool_use so a tool_use/tool_result
470
+ group is never split across the compaction boundary."""
471
+ while idx > 0:
472
+ prev = self._history[idx - 1]
473
+ if (
474
+ prev.role == "assistant"
475
+ and isinstance(prev.content, list)
476
+ and any(isinstance(block, ToolCall) for block in prev.content)
477
+ ):
478
+ idx -= 1
479
+ continue
480
+ break
481
+ return idx
394
482
 
395
483
  # ------------------------------------------------------------------
396
484
  # Serialization
@@ -406,3 +494,26 @@ class Conversation:
406
494
  # Keep history authentic - no fixing here
407
495
  self.history = MessageHistory(parsed_messages)
408
496
  self.sanitize_oversized_tool_results()
497
+
498
+ def dump_compaction(self) -> Dict[str, Any]:
499
+ """Serialize the compaction boundary so it survives save/restore."""
500
+ return {
501
+ "summary": self.summary.get_text_content() if self.summary is not None else "",
502
+ "compacted_through": self.compacted_through,
503
+ }
504
+
505
+ def restore_compaction(self, data: Optional[Dict[str, Any]]) -> None:
506
+ """Restore a compaction boundary saved by ``dump_compaction``.
507
+
508
+ Call AFTER ``restore`` (which resets compaction). Assigns the boundary
509
+ fields directly so it does not trip the history setter's reset.
510
+ """
511
+ data = data or {}
512
+ text = (data.get("summary") or "").strip()
513
+ through = int(data.get("compacted_through") or 0)
514
+ if text and through > 0:
515
+ self.summary = Message(role="user", content=[TextBlock(text=text)])
516
+ self.compacted_through = min(through, len(self._history))
517
+ else:
518
+ self.summary = None
519
+ self.compacted_through = 0
@@ -0,0 +1,28 @@
1
+ You are compacting an in-progress coding session so the work can continue seamlessly in a smaller context window. Produce a TIGHT continuity briefing — not a transcript.
2
+
3
+ Hard rules:
4
+ - Respond with the summary text ONLY. Do NOT call tools. Do NOT ask questions.
5
+ - Be bounded: aim for under ~600 words. Omit anything not needed to keep working.
6
+ - Quote identifiers EXACTLY: file paths, function/class names, variable names, error strings, commands, and config keys. Do not paraphrase code or paths.
7
+ - Do not invent facts. If something is unknown or unfinished, say so briefly.
8
+ - Preserve any `<skill_content name="...">` instructions referenced earlier — note them by name so they are not lost.
9
+
10
+ Write these sections, each terse. Use `##` headers and keep them in this order:
11
+
12
+ ## Goal
13
+ The user's overall objective and any explicit constraints, in 1-3 sentences.
14
+
15
+ ## State
16
+ What is true RIGHT NOW: what has been built or changed, what works, what has been verified.
17
+
18
+ ## Files Touched
19
+ A bullet list of exact paths read/created/modified, each with a 3-8 word note on what changed or why it matters. No code blocks unless a snippet is load-bearing.
20
+
21
+ ## Decisions
22
+ Key technical decisions and their one-line rationale. Note approaches already tried and rejected.
23
+
24
+ ## Open Problems
25
+ Exact open errors (quote them), failing tests, or unresolved questions. Write "None" if there are none.
26
+
27
+ ## Next Steps
28
+ The concrete next actions to take, in order. Write "None" if the task appears complete.
@@ -0,0 +1,17 @@
1
+ {% if previous_summary %}
2
+ A prior summary already covers the earliest part of this session. Treat it as established context and fold its facts into your new summary (do not drop anything important from it):
3
+
4
+ <previous_summary>
5
+ {{ previous_summary }}
6
+ </previous_summary>
7
+
8
+ Below are the messages that have happened SINCE that summary and now need to be folded in as well:
9
+ {% else %}
10
+ Summarize the following coding session so far:
11
+ {% endif %}
12
+
13
+ <conversation_history>
14
+ {{ history }}
15
+ </conversation_history>
16
+
17
+ Produce the continuity briefing exactly as specified in your instructions. Summary text only — do not call tools.
@@ -4,7 +4,7 @@ from __future__ import annotations
4
4
 
5
5
  from functools import lru_cache
6
6
  from pathlib import Path
7
- from typing import Any
7
+ from typing import Any, Optional
8
8
 
9
9
  import jinja2
10
10
 
@@ -56,8 +56,12 @@ SKILL_CATALOG_PROMPT_TEMPLATE = prompt_template_source("extensions/skills/catalo
56
56
  PLANNING_AGENT_SYSTEM_PROMPT_TEMPLATE = prompt_template_source("system/agents/planning.md.j2")
57
57
 
58
58
 
59
- def build_compression_summary_user_prompt(history: str) -> str:
60
- return render_prompt_template("auxiliary/compression/summary.user.md.j2", history=history)
59
+ def build_compression_summary_user_prompt(history: str, previous_summary: Optional[str] = None) -> str:
60
+ return render_prompt_template(
61
+ "auxiliary/compression/summary.user.md.j2",
62
+ history=history,
63
+ previous_summary=previous_summary,
64
+ )
61
65
 
62
66
 
63
67
  def build_implement_plan_prompt(plan: str, gigacode_enabled: bool = False) -> str: