ayder-cli 2.1.1__tar.gz → 2.1.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 (236) hide show
  1. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/PKG-INFO +1 -1
  2. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/docs/config.toml.example +47 -7
  3. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/pyproject.toml +1 -1
  4. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/core/config.py +34 -3
  5. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/core/default_context_manager.py +48 -12
  6. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/providers/impl/ollama.py +4 -6
  7. ayder_cli-2.1.2/tests/core/test_config_provider_logging.py +46 -0
  8. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/core/test_default_context_manager.py +133 -0
  9. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/providers/test_ollama_native.py +33 -0
  10. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/.github/workflows/ci.yml +0 -0
  11. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/.github/workflows/python-package.yml +0 -0
  12. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/.gitignore +0 -0
  13. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/.python-version +0 -0
  14. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/AGENTS.md +0 -0
  15. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/CHANGELOG.md +0 -0
  16. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/LICENSE +0 -0
  17. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/README.md +0 -0
  18. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/docs/PROJECT_STRUCTURE.md +0 -0
  19. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/docs/README_AGENT_WORKFLOW.md +0 -0
  20. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/docs/README_OLLAMA.md +0 -0
  21. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/docs/cc.png +0 -0
  22. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/__init__.py +0 -0
  23. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/__main__.py +0 -0
  24. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/agents/__init__.py +0 -0
  25. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/agents/callbacks.py +0 -0
  26. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/agents/config.py +0 -0
  27. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/agents/registry.py +0 -0
  28. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/agents/run.py +0 -0
  29. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/agents/runner.py +0 -0
  30. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/agents/tool.py +0 -0
  31. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/application/README.md +0 -0
  32. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/application/__init__.py +0 -0
  33. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/application/execution_policy.py +0 -0
  34. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/application/message_contract.py +0 -0
  35. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/application/runtime_factory.py +0 -0
  36. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/application/validation.py +0 -0
  37. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/cli.py +0 -0
  38. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/cli_callbacks.py +0 -0
  39. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/cli_runner.py +0 -0
  40. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/console.py +0 -0
  41. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/core/cache_monitor.py +0 -0
  42. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/core/config_migration.py +0 -0
  43. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/core/context.py +0 -0
  44. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/core/context_manager.py +0 -0
  45. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/core/context_manager_factory.py +0 -0
  46. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/core/ollama_context_manager.py +0 -0
  47. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/core/result.py +0 -0
  48. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/logging_config.py +0 -0
  49. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/loops/__init__.py +0 -0
  50. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/loops/chat_loop.py +0 -0
  51. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/parser.py +0 -0
  52. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/process_manager.py +0 -0
  53. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/prompts.py +0 -0
  54. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/providers/__init__.py +0 -0
  55. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/providers/base.py +0 -0
  56. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/providers/impl/claude.py +0 -0
  57. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/providers/impl/deepseek.py +0 -0
  58. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/providers/impl/gemini.py +0 -0
  59. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/providers/impl/glm.py +0 -0
  60. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/providers/impl/ollama_drivers/__init__.py +0 -0
  61. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/providers/impl/ollama_drivers/_errors.py +0 -0
  62. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/providers/impl/ollama_drivers/base.py +0 -0
  63. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/providers/impl/ollama_drivers/deepseek.py +0 -0
  64. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/providers/impl/ollama_drivers/generic_native.py +0 -0
  65. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/providers/impl/ollama_drivers/generic_xml.py +0 -0
  66. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/providers/impl/ollama_drivers/matrix.py +0 -0
  67. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/providers/impl/ollama_drivers/minimax.py +0 -0
  68. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/providers/impl/ollama_drivers/qwen3.py +0 -0
  69. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/providers/impl/ollama_drivers/registry.py +0 -0
  70. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/providers/impl/ollama_inspector.py +0 -0
  71. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/providers/impl/openai.py +0 -0
  72. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/providers/impl/qwen.py +0 -0
  73. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/providers/orchestrator.py +0 -0
  74. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/providers/retry.py +0 -0
  75. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/services/__init__.py +0 -0
  76. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/services/interactions.py +0 -0
  77. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/themes/__init__.py +0 -0
  78. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/themes/claude.py +0 -0
  79. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/themes/original.py +0 -0
  80. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tools/__init__.py +0 -0
  81. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tools/builtins/__init__.py +0 -0
  82. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tools/builtins/context.py +0 -0
  83. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tools/builtins/context_definitions.py +0 -0
  84. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tools/builtins/filesystem.py +0 -0
  85. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tools/builtins/filesystem_definitions.py +0 -0
  86. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tools/builtins/notes.py +0 -0
  87. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tools/builtins/notes_definitions.py +0 -0
  88. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tools/builtins/process_manager_definitions.py +0 -0
  89. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tools/builtins/search.py +0 -0
  90. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tools/builtins/search_definitions.py +0 -0
  91. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tools/builtins/shell.py +0 -0
  92. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tools/builtins/shell_definitions.py +0 -0
  93. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tools/builtins/tasks.py +0 -0
  94. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tools/builtins/tasks_definitions.py +0 -0
  95. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tools/builtins/utils_tools.py +0 -0
  96. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tools/builtins/utils_tools_definitions.py +0 -0
  97. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tools/builtins/web.py +0 -0
  98. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tools/builtins/web_definitions.py +0 -0
  99. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tools/definition.py +0 -0
  100. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tools/execution.py +0 -0
  101. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tools/hooks.py +0 -0
  102. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tools/normalization.py +0 -0
  103. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tools/plugin_api.py +0 -0
  104. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tools/plugin_github.py +0 -0
  105. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tools/plugin_manager.py +0 -0
  106. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tools/registry.py +0 -0
  107. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tools/schemas.py +0 -0
  108. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tools/utils.py +0 -0
  109. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tui/__init__.py +0 -0
  110. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tui/adapter.py +0 -0
  111. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tui/app.py +0 -0
  112. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tui/commands.py +0 -0
  113. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tui/helpers.py +0 -0
  114. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tui/keybindings.py +0 -0
  115. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tui/parser.py +0 -0
  116. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tui/screens.py +0 -0
  117. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tui/theme_manager.py +0 -0
  118. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tui/types.py +0 -0
  119. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/tui/widgets.py +0 -0
  120. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/ui.py +0 -0
  121. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/src/ayder_cli/version.py +0 -0
  122. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/COVERAGE.md +0 -0
  123. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/__init__.py +0 -0
  124. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/agents/__init__.py +0 -0
  125. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/agents/test_agent_tools.py +0 -0
  126. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/agents/test_callbacks.py +0 -0
  127. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/agents/test_config.py +0 -0
  128. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/agents/test_integration.py +0 -0
  129. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/agents/test_registry.py +0 -0
  130. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/agents/test_run.py +0 -0
  131. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/agents/test_runner.py +0 -0
  132. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/agents/test_runner_provider_unavailable.py +0 -0
  133. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/agents/test_tool.py +0 -0
  134. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/application/test_message_contract.py +0 -0
  135. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/application/test_runtime_factory.py +0 -0
  136. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/client/test_main.py +0 -0
  137. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/convergence/__init__.py +0 -0
  138. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/convergence/test_execution_policy_parity.py +0 -0
  139. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/convergence/test_runtime_wiring.py +0 -0
  140. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/convergence/test_validation_path.py +0 -0
  141. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/core/test_cache_monitor.py +0 -0
  142. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/core/test_config.py +0 -0
  143. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/core/test_config_coverage.py +0 -0
  144. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/core/test_config_migration.py +0 -0
  145. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/core/test_config_retry.py +0 -0
  146. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/core/test_config_v2.py +0 -0
  147. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/core/test_context_manager_factory.py +0 -0
  148. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/core/test_default_context_manager_should_compact.py +0 -0
  149. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/core/test_ollama_context_manager.py +0 -0
  150. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/core/test_ollama_context_manager_cache_monitor.py +0 -0
  151. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/core/test_ollama_context_manager_max_history.py +0 -0
  152. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/core/test_parameter_aliasing.py +0 -0
  153. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/core/test_parser.py +0 -0
  154. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/loops/__init__.py +0 -0
  155. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/loops/test_base.py +0 -0
  156. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/loops/test_chat_loop_hook.py +0 -0
  157. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/loops/test_chat_loop_tool_truncation.py +0 -0
  158. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/providers/__init__.py +0 -0
  159. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/providers/ollama_drivers/__init__.py +0 -0
  160. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/providers/ollama_drivers/test_base.py +0 -0
  161. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/providers/ollama_drivers/test_deepseek.py +0 -0
  162. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/providers/ollama_drivers/test_errors.py +0 -0
  163. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/providers/ollama_drivers/test_fallback.py +0 -0
  164. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/providers/ollama_drivers/test_generic_native.py +0 -0
  165. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/providers/ollama_drivers/test_generic_xml.py +0 -0
  166. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/providers/ollama_drivers/test_matrix.py +0 -0
  167. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/providers/ollama_drivers/test_minimax.py +0 -0
  168. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/providers/ollama_drivers/test_provider_integration.py +0 -0
  169. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/providers/ollama_drivers/test_qwen3.py +0 -0
  170. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/providers/ollama_drivers/test_registry.py +0 -0
  171. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/providers/test_glm_async.py +0 -0
  172. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/providers/test_ollama_inspector.py +0 -0
  173. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/providers/test_ollama_xml.py +0 -0
  174. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/providers/test_ollama_xml_autoroute.py +0 -0
  175. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/providers/test_ollama_xml_usage.py +0 -0
  176. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/providers/test_orchestrator_capabilities.py +0 -0
  177. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/providers/test_qwen_async.py +0 -0
  178. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/providers/test_retry_classifier.py +0 -0
  179. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/providers/test_retry_integration.py +0 -0
  180. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/providers/test_retrying_provider.py +0 -0
  181. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/services/test_boundary.py +0 -0
  182. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/services/test_interaction_sink.py +0 -0
  183. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/test_cli.py +0 -0
  184. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/test_cli_callbacks.py +0 -0
  185. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/test_context.py +0 -0
  186. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/test_env_manager.py +0 -0
  187. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/test_logging_config.py +0 -0
  188. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/test_notes.py +0 -0
  189. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/test_process_manager.py +0 -0
  190. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/test_tasks.py +0 -0
  191. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/tools/__init__.py +0 -0
  192. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/tools/test_agent_notes.py +0 -0
  193. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/tools/test_definition_discovery.py +0 -0
  194. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/tools/test_di_injection.py +0 -0
  195. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/tools/test_impl.py +0 -0
  196. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/tools/test_impl_coverage.py +0 -0
  197. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/tools/test_path_security.py +0 -0
  198. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/tools/test_plugin_api.py +0 -0
  199. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/tools/test_plugin_github.py +0 -0
  200. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/tools/test_plugin_loading.py +0 -0
  201. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/tools/test_plugin_manager.py +0 -0
  202. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/tools/test_registry.py +0 -0
  203. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/tools/test_registry_coverage.py +0 -0
  204. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/tools/test_result.py +0 -0
  205. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/tools/test_schemas.py +0 -0
  206. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/tools/test_search_codebase.py +0 -0
  207. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/tools/test_utils.py +0 -0
  208. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/tools/test_web.py +0 -0
  209. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/tui/__init__.py +0 -0
  210. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/tui/test_agent_list_screen.py +0 -0
  211. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/tui/test_do_clear_coordination.py +0 -0
  212. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/tui/test_follow_mode.py +0 -0
  213. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/tui/test_help_screen.py +0 -0
  214. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/tui/test_keybindings.py +0 -0
  215. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/tui/test_multi_select_screen.py +0 -0
  216. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/tui/test_multiline_input.py +0 -0
  217. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/tui/test_paste.py +0 -0
  218. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/tui/test_pending_compact_consumer.py +0 -0
  219. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/tui/test_pull_delivery.py +0 -0
  220. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/tui/test_turn_consumer.py +0 -0
  221. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/tui/test_widgets.py +0 -0
  222. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/ui/test_confirm_screen.py +0 -0
  223. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/ui/test_diff_preview.py +0 -0
  224. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/ui/test_provider_switch.py +0 -0
  225. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/ui/test_run_tui_startup.py +0 -0
  226. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/ui/test_tui_assistant_sanitization.py +0 -0
  227. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/ui/test_tui_chat_loop.py +0 -0
  228. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/ui/test_tui_commands_agent.py +0 -0
  229. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/ui/test_tui_commands_logging.py +0 -0
  230. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/ui/test_tui_commands_provider.py +0 -0
  231. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/ui/test_tui_helpers.py +0 -0
  232. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/ui/test_tui_temporal_command.py +0 -0
  233. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/ui/test_tui_widgets.py +0 -0
  234. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/ui/test_ui.py +0 -0
  235. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/ui/test_ui_coverage.py +0 -0
  236. {ayder_cli-2.1.1 → ayder_cli-2.1.2}/tests/ui/test_ui_verbose.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ayder-cli
3
- Version: 2.1.1
3
+ Version: 2.1.2
4
4
  Summary: AI agent for any LLMs
5
5
  Project-URL: Homepage, https://github.com/ayder/ayder-cli
6
6
  Project-URL: Repository, https://github.com/ayder/ayder-cli.git
@@ -48,9 +48,29 @@
48
48
  # `export OLLAMA_API_KEY="<key>"`. The api_key config field is INERT on the
49
49
  # native path, so a missing env var ⇒ 401 Unauthorized.
50
50
  #
51
- # NOTE on `think`: value is model/driver-dependent. Recent Ollama accepts
52
- # true/false for most models and "low"|"medium"|"high" for reasoning
53
- # models like deepseek-v4-pro. Adjust per model if a value is rejected.
51
+ # NOTE on `think`: on the native `ollama` driver, ayder hardcodes thinking ON
52
+ # (think=True) and does NOT forward the configured level — so a
53
+ # `think = "high"` below documents intent, but the "low/medium/high"
54
+ # granularity is not applied. The `openai` driver sends no reasoning-effort
55
+ # parameter at all.
56
+ #
57
+ # NOTE on tuning knobs — which actually take effect depends on the DRIVER. These
58
+ # agents run on the native `ollama` driver, which ignores most of them:
59
+ # • temperature — forwarded by NO driver yet ⇒ no effect anywhere.
60
+ # • max_output_tokens — NOT sent on the ollama path (Ollama sizes
61
+ # generation itself); honored only by the
62
+ # openai-family drivers as max_tokens. Default 4096.
63
+ # • think — hardcoded ON for ollama (level ignored); openai
64
+ # sends none.
65
+ # • num_ctx — on ollama it is provisional only (the real window
66
+ # is auto-detected from /api/show); on the openai
67
+ # path it sizes the context-manager budget.
68
+ # Default 65536.
69
+ # The per-agent temperature / max_output_tokens / think values below are
70
+ # therefore largely INERT on this Ollama pipeline — they are kept to
71
+ # document intent and to carry over if you switch an agent to an `openai`
72
+ # profile. To actually tune these, use the `openai` driver — see
73
+ # [llm.openai] below.
54
74
  # =============================================================================
55
75
 
56
76
  config_version = "2.0"
@@ -77,10 +97,16 @@ retention = "7 days"
77
97
 
78
98
  [context_manager]
79
99
  enabled = true
80
- # On the native Ollama path, OllamaContextManager auto-detects the model's real
81
- # window from /api/show; max_context_tokens is ignored there and only matters on
82
- # the openai/non-ollama path. reserve_ratio + compaction_threshold ARE read.
83
- max_context_tokens = 131072
100
+ # Destructive tool-result compression is OPT-IN and OFF by default. It summarizes
101
+ # old tool results in place (lossy) and thrashes the KV cache, so leave it off
102
+ # unless you specifically want it; eviction will replace it in a later release.
103
+ enable_compression = false
104
+ # max_context_tokens is OPTIONAL. When unset (recommended), the budget is derived
105
+ # from the active model's num_ctx. It is read only on the openai/non-ollama path
106
+ # (DefaultContextManager); the native Ollama path auto-detects the real window
107
+ # from /api/show and ignores it. reserve_ratio + compaction_threshold ARE read on
108
+ # both paths.
109
+ # max_context_tokens = 131072
84
110
  reserve_ratio = 0.30
85
111
  compaction_threshold = 0.80
86
112
 
@@ -118,6 +144,20 @@ num_ctx = 32768 # keep modest: local VRAM is the constraint
118
144
  # model = "gemma4:31b-cloud"
119
145
  # num_ctx = 131072
120
146
 
147
+ # OpenAI-compatible driver. Unlike the `ollama` driver above, this one HONORS the
148
+ # tuning knobs (defaults shown). Use it for api.openai.com, Azure, vLLM,
149
+ # llama.cpp, LM Studio, or Ollama's own `/v1` endpoint. Point an agent at it with
150
+ # `provider = "openai"`.
151
+ # [llm.openai]
152
+ # driver = "openai"
153
+ # base_url = "https://api.openai.com/v1" # OpenAI-compatible endpoint
154
+ # api_key = "sk-..." # your key (or "ollama"/"none" for local servers)
155
+ # model = "gpt-5.5"
156
+ # num_ctx = 65536 # sizes the context-manager budget (default 65536); NOT sent to the API
157
+ # max_output_tokens = 8192 # -> max_tokens / max_completion_tokens (default 4096); keep WELL below num_ctx
158
+ # stop_sequences = [] # -> stop (default [])
159
+ # # temperature / think are NOT applied by the openai driver today (no-ops) — omitted on purpose.
160
+
121
161
  # =============================================================================
122
162
  # Agents
123
163
  # =============================================================================
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "ayder-cli"
7
- version = "2.1.1"
7
+ version = "2.1.2"
8
8
  description = "AI agent for any LLMs"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12"
@@ -1,8 +1,11 @@
1
+ import logging
1
2
  import tomllib
2
3
  from pathlib import Path
3
4
  from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
4
5
  from typing import Any, Callable, Dict
5
6
 
7
+ logger = logging.getLogger(__name__)
8
+
6
9
  CONFIG_DIR = Path("~/.ayder").expanduser()
7
10
  CONFIG_PATH = CONFIG_DIR / "config.toml"
8
11
 
@@ -174,12 +177,19 @@ class ContextManagerConfigSection(BaseModel):
174
177
  model_config = ConfigDict(frozen=True)
175
178
 
176
179
  enabled: bool = Field(default=True)
177
- max_context_tokens: int = Field(default=8192)
180
+ # Optional: when unset, the context manager derives the budget from the
181
+ # model's num_ctx. The old fixed 8192 default predates large-context models.
182
+ max_context_tokens: int | None = Field(default=None)
178
183
  reserve_ratio: float = Field(default=0.30)
179
184
  compaction_threshold: float = Field(default=0.70)
180
185
  tool_result_compress_age: int = Field(default=5)
181
186
  max_tool_result_length: int = Field(default=2048)
182
187
  compress_tool_results: bool = Field(default=True)
188
+ # Master switch for destructive tool-result compression. Compression is
189
+ # known to be too aggressive (it summarizes old tool results in place and
190
+ # thrashes the KV cache), so it is OPT-IN: disabled unless explicitly set.
191
+ # Eviction will replace it in a later phase.
192
+ enable_compression: bool = Field(default=False)
183
193
 
184
194
  @field_validator("reserve_ratio", "compaction_threshold")
185
195
  @classmethod
@@ -190,8 +200,8 @@ class ContextManagerConfigSection(BaseModel):
190
200
 
191
201
  @field_validator("max_context_tokens")
192
202
  @classmethod
193
- def validate_positive(cls, v: int) -> int:
194
- if v <= 0:
203
+ def validate_positive(cls, v: int | None) -> int | None:
204
+ if v is not None and v <= 0:
195
205
  raise ValueError("max_context_tokens must be positive")
196
206
  return v
197
207
 
@@ -309,9 +319,25 @@ class Config(BaseModel):
309
319
  if isinstance(llm_section, dict):
310
320
  profile = llm_section.get(provider)
311
321
  if not isinstance(profile, dict):
322
+ available = ", ".join(sorted(llm_section)) or "<none>"
312
323
  default_profile = llm_section.get(DEFAULTS["provider"])
313
324
  if isinstance(default_profile, dict):
314
325
  profile = default_profile
326
+ logger.error(
327
+ "Provider %r has no [llm.%s] profile; falling back to the "
328
+ "default [llm.%s] profile. Available profiles: %s. Fix the "
329
+ "provider name or add an [llm.%s] section.",
330
+ provider, provider, DEFAULTS["provider"], available, provider,
331
+ )
332
+ else:
333
+ logger.error(
334
+ "Provider %r has no [llm.%s] profile and no default "
335
+ "[llm.%s] profile exists; the driver will default to "
336
+ "'openai' with built-in endpoint defaults. Available "
337
+ "profiles: %s. Fix the provider name or add an [llm.%s] "
338
+ "section.",
339
+ provider, provider, DEFAULTS["provider"], available, provider,
340
+ )
315
341
  if isinstance(profile, dict):
316
342
  new_data.update(profile)
317
343
  new_data.pop("llm", None)
@@ -359,6 +385,11 @@ class Config(BaseModel):
359
385
  "qwen", "dashscope", "glm", "zhipu",
360
386
  )
361
387
  if v not in valid:
388
+ logger.error(
389
+ "Unsupported driver %r; must be one of: %s. Fix the driver in "
390
+ "your [llm.<provider>] profile.",
391
+ v, ", ".join(valid),
392
+ )
362
393
  raise ValueError(f"driver must be one of: {', '.join(valid)}")
363
394
  return v
364
395
 
@@ -19,6 +19,10 @@ if TYPE_CHECKING:
19
19
 
20
20
  logger = logging.getLogger(__name__)
21
21
 
22
+ # Fallback context budget used only when neither an explicit max_context_tokens
23
+ # nor a model num_ctx is available (e.g. a bare config section in tests).
24
+ DEFAULT_MAX_CONTEXT_TOKENS = 65536
25
+
22
26
 
23
27
  class MessageTier(Enum):
24
28
  """Importance tiers for messages."""
@@ -147,10 +151,34 @@ class DefaultContextManager:
147
151
  elif isinstance(config, ContextManagerConfigSection):
148
152
  self._config = config
149
153
  else:
150
- max_tokens = getattr(config, "num_ctx", 8192)
151
- if max_tokens <= 0:
152
- max_tokens = 8192
153
- self._config = ContextManagerConfigSection(max_context_tokens=max_tokens)
154
+ self._config = ContextManagerConfigSection()
155
+
156
+ # Resolve the effective context budget. An explicit max_context_tokens
157
+ # wins; otherwise derive it from the model's num_ctx so the budget tracks
158
+ # the real window instead of the legacy fixed default.
159
+ configured = self._config.max_context_tokens
160
+ num_ctx = getattr(config, "num_ctx", None)
161
+ if isinstance(configured, int) and configured > 0:
162
+ self._max_context_tokens = configured
163
+ logger.info(
164
+ "Context budget: %d tokens (explicit max_context_tokens; num_ctx=%s)",
165
+ configured, num_ctx,
166
+ )
167
+ elif isinstance(num_ctx, int) and num_ctx > 0:
168
+ self._max_context_tokens = num_ctx
169
+ logger.info(
170
+ "Context budget: %d tokens (derived from num_ctx; "
171
+ "max_context_tokens unset)",
172
+ num_ctx,
173
+ )
174
+ else:
175
+ self._max_context_tokens = DEFAULT_MAX_CONTEXT_TOKENS
176
+ logger.warning(
177
+ "Context budget: could not read num_ctx or max_context_tokens; "
178
+ "falling back to default %d tokens. Recommend setting num_ctx "
179
+ "(or [context_manager] max_context_tokens) in your config.",
180
+ DEFAULT_MAX_CONTEXT_TOKENS,
181
+ )
154
182
 
155
183
  provider = getattr(config, "provider", "openai")
156
184
  self._counter = TokenCounter(model=model, provider=provider)
@@ -214,7 +242,7 @@ class DefaultContextManager:
214
242
  response arrives. See opus47.md finding #6.
215
243
  """
216
244
  if self._last_prompt_tokens:
217
- ceiling = self._config.max_context_tokens
245
+ ceiling = self._max_context_tokens
218
246
  return self._last_prompt_tokens >= ceiling * self._config.compaction_threshold
219
247
  return self.should_trigger_summarization()
220
248
 
@@ -227,8 +255,8 @@ class DefaultContextManager:
227
255
  return self._stats_cache
228
256
 
229
257
  total = self._calculate_total_tokens()
230
- available = max(0, self._config.max_context_tokens - total)
231
- utilization = (total / self._config.max_context_tokens) * 100
258
+ available = max(0, self._max_context_tokens - total)
259
+ utilization = (total / self._max_context_tokens) * 100
232
260
 
233
261
  self._stats_cache = ContextStats(
234
262
  total_tokens=total,
@@ -245,7 +273,7 @@ class DefaultContextManager:
245
273
 
246
274
  @property
247
275
  def reserve(self) -> int:
248
- return int(self._config.max_context_tokens * self._config.reserve_ratio)
276
+ return int(self._max_context_tokens * self._config.reserve_ratio)
249
277
 
250
278
  def count_tokens(self, text: str) -> int:
251
279
  """Legacy support."""
@@ -362,18 +390,24 @@ class DefaultContextManager:
362
390
  return False
363
391
 
364
392
  total = self._calculate_total_tokens()
365
- utilization = total / self._config.max_context_tokens
393
+ utilization = total / self._max_context_tokens
366
394
  return utilization >= self._config.compaction_threshold
367
395
 
368
396
  def should_trigger_compression(self) -> bool:
397
+ if not getattr(self._config, "enable_compression", False):
398
+ return False
369
399
  if not self._config.enabled or not self._config.compress_tool_results:
370
400
  return False
371
401
 
372
402
  total = self._calculate_total_tokens()
373
- utilization = total / self._config.max_context_tokens
403
+ utilization = total / self._max_context_tokens
374
404
  return utilization >= self._config.compaction_threshold
375
405
 
376
406
  def compress_old_results(self, age_threshold: int | None = None) -> int:
407
+ # Compression is opt-in. Disabled by default because it destructively
408
+ # summarizes old tool results in place and thrashes the KV cache.
409
+ if not getattr(self._config, "enable_compression", False):
410
+ return 0
377
411
  threshold = (
378
412
  age_threshold
379
413
  if age_threshold is not None
@@ -449,10 +483,12 @@ class DefaultContextManager:
449
483
  return overhead
450
484
 
451
485
  def _available_budget(self, overhead: int) -> int:
452
- reserve = int(self._config.max_context_tokens * self._config.reserve_ratio)
453
- return self._config.max_context_tokens - overhead - reserve
486
+ reserve = int(self._max_context_tokens * self._config.reserve_ratio)
487
+ return self._max_context_tokens - overhead - reserve
454
488
 
455
489
  def _auto_compress(self) -> None:
490
+ if not getattr(self._config, "enable_compression", False):
491
+ return
456
492
  if not self._config.compress_tool_results:
457
493
  return
458
494
  if self.should_trigger_compression():
@@ -146,8 +146,8 @@ class OllamaProvider(AIProvider):
146
146
  tools: list[dict[str, Any]] | None,
147
147
  options: dict[str, Any] | None,
148
148
  ) -> AsyncGenerator[NormalizedStreamChunk, None]:
149
- opts = options or {}
150
- num_ctx = opts.get("num_ctx", 65536)
149
+ # Let Ollama size the context window from the model itself (recent
150
+ # versions do this automatically); do not force num_ctx.
151
151
  ollama_tools = self._convert_tools(tools) if tools else None
152
152
  ollama_messages = self._convert_messages(messages)
153
153
 
@@ -155,7 +155,6 @@ class OllamaProvider(AIProvider):
155
155
  model=model,
156
156
  messages=ollama_messages,
157
157
  tools=ollama_tools,
158
- options={"num_ctx": num_ctx},
159
158
  keep_alive=-1,
160
159
  think=True,
161
160
  stream=True,
@@ -204,8 +203,8 @@ class OllamaProvider(AIProvider):
204
203
  tools: list[dict[str, Any]] | None,
205
204
  options: dict[str, Any] | None,
206
205
  ) -> AsyncGenerator[NormalizedStreamChunk, None]:
207
- opts = options or {}
208
- num_ctx = opts.get("num_ctx", 65536)
206
+ # Let Ollama size the context window from the model itself; do not
207
+ # force num_ctx.
209
208
  formatted_messages = driver.render_tools_into_messages(messages, tools or [])
210
209
  ollama_messages = self._convert_messages(formatted_messages)
211
210
 
@@ -213,7 +212,6 @@ class OllamaProvider(AIProvider):
213
212
  model=model,
214
213
  messages=ollama_messages,
215
214
  tools=None,
216
- options={"num_ctx": num_ctx},
217
215
  keep_alive=-1,
218
216
  think=True,
219
217
  stream=True,
@@ -0,0 +1,46 @@
1
+ """Config should log an actionable error when a provider/driver is misconfigured."""
2
+ import logging
3
+
4
+ import pytest
5
+ from pydantic import ValidationError
6
+
7
+ from ayder_cli.core.config import Config
8
+
9
+
10
+ def test_missing_provider_profile_logs_error(caplog):
11
+ """A provider with no matching [llm.<provider>] profile logs an error and
12
+ still falls back to the openai driver (so the run does not silently misroute
13
+ without any trace)."""
14
+ data = {
15
+ "app": {"provider": "ollama_cloud"},
16
+ "llm": {"ollama": {"driver": "ollama", "model": "m"}},
17
+ }
18
+ with caplog.at_level(logging.ERROR):
19
+ cfg = Config(**data)
20
+
21
+ assert cfg.driver == "openai" # silent fallback — now logged
22
+ text = caplog.text
23
+ assert "ollama_cloud" in text
24
+ assert "ollama" in text # names the available profile / section to add
25
+
26
+
27
+ def test_valid_provider_profile_logs_no_error(caplog):
28
+ """A provider that has a matching profile must not log an error."""
29
+ data = {
30
+ "app": {"provider": "ollama"},
31
+ "llm": {"ollama": {"driver": "ollama", "model": "m"}},
32
+ }
33
+ with caplog.at_level(logging.ERROR):
34
+ cfg = Config(**data)
35
+
36
+ assert cfg.driver == "ollama"
37
+ assert caplog.text == ""
38
+
39
+
40
+ def test_invalid_driver_logs_error(caplog):
41
+ """An unsupported driver name logs an error before raising."""
42
+ with caplog.at_level(logging.ERROR):
43
+ with pytest.raises(ValidationError):
44
+ Config(driver="not_a_real_driver", provider="openai")
45
+
46
+ assert "not_a_real_driver" in caplog.text
@@ -178,6 +178,139 @@ def test_group_into_units():
178
178
  assert units[2] == [messages[3]]
179
179
 
180
180
 
181
+ def _make_old_long_tool_result_manager(config):
182
+ """Manager holding one old, long tool result eligible for compression."""
183
+ mgr = DefaultContextManager(config)
184
+ long_content = "x" * 5000 # exceeds max_tool_result_length (2048)
185
+ mgr.add_message({"role": "user", "content": "go"})
186
+ mgr.add_message(
187
+ {
188
+ "role": "assistant",
189
+ "content": "",
190
+ "tool_calls": [
191
+ {"id": "1", "function": {"name": "read_file", "arguments": "{}"}}
192
+ ],
193
+ }
194
+ )
195
+ mgr.add_message(
196
+ {"role": "tool", "tool_call_id": "1", "name": "read_file", "content": long_content}
197
+ )
198
+ # Filler so the tool result is "old" (age > tool_result_compress_age=5).
199
+ for i in range(10):
200
+ mgr.add_message({"role": "user", "content": f"m{i}"})
201
+ return mgr, long_content
202
+
203
+
204
+ def test_max_context_tokens_defaults_to_none():
205
+ """max_context_tokens is now optional — unset means 'derive from num_ctx'."""
206
+ from ayder_cli.core.config import ContextManagerConfigSection
207
+
208
+ assert ContextManagerConfigSection().max_context_tokens is None
209
+
210
+
211
+ def test_budget_syncs_from_num_ctx_when_max_context_tokens_unset():
212
+ """When max_context_tokens is unset, the budget tracks the model's num_ctx."""
213
+ from types import SimpleNamespace
214
+ from ayder_cli.core.config import ContextManagerConfigSection
215
+
216
+ cfg = SimpleNamespace(
217
+ context_manager=ContextManagerConfigSection(), # max_context_tokens=None
218
+ num_ctx=131072,
219
+ provider="openai",
220
+ model="x",
221
+ )
222
+ mgr = DefaultContextManager(cfg)
223
+
224
+ assert mgr._max_context_tokens == 131072
225
+ assert mgr.reserve == int(131072 * 0.30)
226
+
227
+
228
+ def test_budget_fallback_to_default_logs_warning(caplog):
229
+ """When neither max_context_tokens nor num_ctx is readable, warn + use default."""
230
+ import logging
231
+ from ayder_cli.core.config import ContextManagerConfigSection
232
+ from ayder_cli.core.default_context_manager import DEFAULT_MAX_CONTEXT_TOKENS
233
+
234
+ config = ContextManagerConfigSection() # max_context_tokens=None, no num_ctx attr
235
+ with caplog.at_level(logging.WARNING):
236
+ mgr = DefaultContextManager(config)
237
+
238
+ assert mgr._max_context_tokens == DEFAULT_MAX_CONTEXT_TOKENS
239
+ assert "num_ctx" in caplog.text
240
+ assert str(DEFAULT_MAX_CONTEXT_TOKENS) in caplog.text
241
+
242
+
243
+ def test_budget_derived_from_num_ctx_logs_info(caplog):
244
+ """Deriving the budget from num_ctx is reported at INFO with both values."""
245
+ import logging
246
+ from types import SimpleNamespace
247
+ from ayder_cli.core.config import ContextManagerConfigSection
248
+
249
+ cfg = SimpleNamespace(
250
+ context_manager=ContextManagerConfigSection(),
251
+ num_ctx=131072,
252
+ provider="openai",
253
+ model="x",
254
+ )
255
+ with caplog.at_level(logging.INFO):
256
+ DefaultContextManager(cfg)
257
+
258
+ assert "131072" in caplog.text
259
+ assert "num_ctx" in caplog.text
260
+
261
+
262
+ def test_explicit_max_context_tokens_overrides_num_ctx():
263
+ """An explicit max_context_tokens wins over num_ctx."""
264
+ from types import SimpleNamespace
265
+ from ayder_cli.core.config import ContextManagerConfigSection
266
+
267
+ cfg = SimpleNamespace(
268
+ context_manager=ContextManagerConfigSection(max_context_tokens=4096),
269
+ num_ctx=131072,
270
+ provider="openai",
271
+ model="x",
272
+ )
273
+ mgr = DefaultContextManager(cfg)
274
+
275
+ assert mgr._max_context_tokens == 4096
276
+
277
+
278
+ def test_enable_compression_defaults_off():
279
+ """Compression must be opt-in: the global flag defaults to False."""
280
+ from ayder_cli.core.config import ContextManagerConfigSection
281
+
282
+ assert ContextManagerConfigSection().enable_compression is False
283
+
284
+
285
+ def test_compress_old_results_noop_when_compression_disabled():
286
+ """With the default (disabled) flag, old long tool results are NOT compressed."""
287
+ from ayder_cli.core.config import ContextManagerConfigSection
288
+
289
+ config = ContextManagerConfigSection(max_context_tokens=1000)
290
+ mgr, original = _make_old_long_tool_result_manager(config)
291
+
292
+ compressed = mgr.compress_old_results()
293
+
294
+ assert compressed == 0
295
+ assert mgr._messages[2]["content"] == original # untouched
296
+
297
+
298
+ def test_compress_old_results_runs_when_compression_enabled():
299
+ """When enable_compression=True, eligible old long tool results are compressed."""
300
+ from ayder_cli.core.config import ContextManagerConfigSection
301
+
302
+ config = ContextManagerConfigSection(
303
+ max_context_tokens=1000, enable_compression=True
304
+ )
305
+ mgr, original = _make_old_long_tool_result_manager(config)
306
+
307
+ compressed = mgr.compress_old_results()
308
+
309
+ assert compressed == 1
310
+ assert mgr._messages[2]["content"] != original
311
+ assert "truncated" in mgr._messages[2]["content"].lower()
312
+
313
+
181
314
  def test_assign_tier_user_messages_never_old_assistant():
182
315
  """_assign_tier must not assign OLD_ASSISTANT to old user messages."""
183
316
  from ayder_cli.core.config import ContextManagerConfigSection
@@ -204,6 +204,39 @@ async def test_keep_alive_set_to_infinite():
204
204
  assert call_kwargs.kwargs.get("keep_alive") == -1 or call_kwargs[1].get("keep_alive") == -1
205
205
 
206
206
 
207
+ @pytest.mark.asyncio
208
+ async def test_native_stream_does_not_force_num_ctx():
209
+ """Provider must let Ollama derive context from the model, not force num_ctx.
210
+
211
+ Recent Ollama versions size the context window from the model itself, so the
212
+ provider should not override it via options.
213
+ """
214
+ cfg = make_config()
215
+
216
+ async def mock_stream(*args, **kwargs):
217
+ yield make_chat_response(content="ok", done=True,
218
+ prompt_eval_count=1, eval_count=1,
219
+ prompt_eval_duration=10, eval_duration=10,
220
+ load_duration=0)
221
+
222
+ with patch("ayder_cli.providers.impl.ollama.AsyncClient") as MockClient:
223
+ instance = AsyncMock()
224
+ instance.chat.return_value = mock_stream()
225
+ MockClient.return_value = instance
226
+
227
+ provider = OllamaProvider(cfg)
228
+ async for _ in provider.stream_with_tools(
229
+ messages=[{"role": "user", "content": "test"}],
230
+ model="test:latest",
231
+ tools=None,
232
+ options={"num_ctx": 4096},
233
+ ):
234
+ pass
235
+
236
+ sent_options = instance.chat.call_args.kwargs.get("options")
237
+ assert not (sent_options and "num_ctx" in sent_options)
238
+
239
+
207
240
  # =============================================================================
208
241
  # SDK Compliance Regression Tests
209
242
  # =============================================================================
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes