henchman-ai 0.1.6__tar.gz → 0.1.7__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 (219) hide show
  1. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/PKG-INFO +1 -1
  2. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/pyproject.toml +1 -1
  3. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/tools/builtins/glob_tool.py +4 -4
  4. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/tools/builtins/grep.py +2 -5
  5. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/tools/builtins/ls.py +6 -6
  6. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/tools/builtins/shell.py +1 -1
  7. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/utils/compaction.py +30 -12
  8. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/utils/tokens.py +24 -0
  9. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/version.py +1 -1
  10. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/e2e/test_context_safety.py +29 -27
  11. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/test_main.py +1 -1
  12. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/test_version.py +1 -1
  13. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/.github/copilot-instructions.md +0 -0
  14. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/.github/workflows/ci.yml +0 -0
  15. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/.github/workflows/publish.yml +0 -0
  16. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/.gitignore +0 -0
  17. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/400_error.md +0 -0
  18. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/400_error_final_report.md +0 -0
  19. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/400_error_fix_report.md +0 -0
  20. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/ATTRIBUTE_ERROR_FIX_SUMMARY.md +0 -0
  21. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/CHANGELOG.md +0 -0
  22. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/CI_100_PERCENT_PASS_RATE_ANALYSIS.md +0 -0
  23. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/CI_FIX_ACTION_PLAN.md +0 -0
  24. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/CI_FIX_PROGRESS_SUMMARY.md +0 -0
  25. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/CI_PIPELINE_COMPREHENSIVE_ANALYSIS.md +0 -0
  26. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/FUNCTIONAL_TEST_IMPLEMENTATION_SUMMARY.md +0 -0
  27. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/IMPLEMENTATION_PLAN.md +0 -0
  28. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/INTEGRATION_TESTING.md +0 -0
  29. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/INTEGRATION_TESTING_PROGRESS_SUMMARY.md +0 -0
  30. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/INTEGRATION_TESTING_TASK_COMPLETION.md +0 -0
  31. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/KEYBOARD_INTERRUPT_FIXES_SUMMARY.md +0 -0
  32. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/LICENSE +0 -0
  33. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/README.md +0 -0
  34. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/TASK_COMPLETION_SUMMARY.md +0 -0
  35. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/UI_INTEGRATION_TESTING_SUMMARY.md +0 -0
  36. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/UI_TOOL_CALLS_SUMMARY.md +0 -0
  37. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/analyze_400_error.py +0 -0
  38. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/compaction_bug_analysis.md +0 -0
  39. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/debug_compaction.py +0 -0
  40. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/detailed_400_analysis.md +0 -0
  41. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/docs/api.md +0 -0
  42. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/docs/configuration.md +0 -0
  43. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/docs/extensions.md +0 -0
  44. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/docs/getting-started.md +0 -0
  45. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/docs/index.md +0 -0
  46. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/docs/mcp.md +0 -0
  47. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/docs/providers.md +0 -0
  48. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/docs/tools.md +0 -0
  49. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/fix_repl.py +0 -0
  50. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/fix_repl_simple.py +0 -0
  51. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/mkdocs.yml +0 -0
  52. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/replace_method.py +0 -0
  53. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/reproduce_400_error.py +0 -0
  54. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/run_interactive_tests.py +0 -0
  55. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/scripts/ci.sh +0 -0
  56. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/__init__.py +0 -0
  57. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/__main__.py +0 -0
  58. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/cli/__init__.py +0 -0
  59. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/cli/app.py +0 -0
  60. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/cli/commands/__init__.py +0 -0
  61. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/cli/commands/builtins.py +0 -0
  62. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/cli/commands/chat.py +0 -0
  63. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/cli/commands/extensions.py +0 -0
  64. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/cli/commands/mcp.py +0 -0
  65. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/cli/commands/plan.py +0 -0
  66. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/cli/commands/skill.py +0 -0
  67. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/cli/console.py +0 -0
  68. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/cli/input.py +0 -0
  69. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/cli/json_output.py +0 -0
  70. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/cli/prompts.py +0 -0
  71. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/cli/repl.py +0 -0
  72. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/cli/repl.py.backup +0 -0
  73. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/cli/repl.py.backup2 +0 -0
  74. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/config/__init__.py +0 -0
  75. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/config/context.py +0 -0
  76. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/config/schema.py +0 -0
  77. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/config/settings.py +0 -0
  78. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/core/__init__.py +0 -0
  79. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/core/agent.py +0 -0
  80. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/core/agent.py.backup +0 -0
  81. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/core/events.py +0 -0
  82. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/core/session.py +0 -0
  83. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/extensions/__init__.py +0 -0
  84. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/extensions/base.py +0 -0
  85. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/extensions/manager.py +0 -0
  86. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/mcp/__init__.py +0 -0
  87. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/mcp/client.py +0 -0
  88. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/mcp/config.py +0 -0
  89. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/mcp/manager.py +0 -0
  90. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/mcp/tool.py +0 -0
  91. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/providers/__init__.py +0 -0
  92. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/providers/anthropic.py +0 -0
  93. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/providers/base.py +0 -0
  94. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/providers/deepseek.py +0 -0
  95. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/providers/ollama.py +0 -0
  96. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/providers/openai_compat.py +0 -0
  97. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/providers/openai_compat.py.backup +0 -0
  98. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/providers/registry.py +0 -0
  99. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/skills/__init__.py +0 -0
  100. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/skills/executor.py +0 -0
  101. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/skills/learner.py +0 -0
  102. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/skills/models.py +0 -0
  103. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/skills/store.py +0 -0
  104. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/tools/__init__.py +0 -0
  105. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/tools/base.py +0 -0
  106. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/tools/builtins/__init__.py +0 -0
  107. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/tools/builtins/ask_user.py +0 -0
  108. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/tools/builtins/file_edit.py +0 -0
  109. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/tools/builtins/file_read.py +0 -0
  110. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/tools/builtins/file_write.py +0 -0
  111. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/tools/builtins/web_fetch.py +0 -0
  112. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/tools/registry.py +0 -0
  113. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/utils/__init__.py +0 -0
  114. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/utils/validation.py +0 -0
  115. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/test_compaction.py +0 -0
  116. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/test_compaction_fix.py +0 -0
  117. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/test_fixes.py +0 -0
  118. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/test_output.txt +0 -0
  119. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/test_run.py +0 -0
  120. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/__init__.py +0 -0
  121. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/__init__.py +0 -0
  122. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/commands/test_plan.py +0 -0
  123. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/commands/test_skill.py +0 -0
  124. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/commands/test_skill_extended.py +0 -0
  125. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_app.py +0 -0
  126. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_app_extended.py +0 -0
  127. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_builtins.py +0 -0
  128. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_chat_command.py +0 -0
  129. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_cli_smoke.py +0 -0
  130. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_commands.py +0 -0
  131. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_commands_repro.py +0 -0
  132. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_console.py +0 -0
  133. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_enhanced_tool_display.py +0 -0
  134. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_input.py +0 -0
  135. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_input_bindings.py +0 -0
  136. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_json_output.py +0 -0
  137. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_keyboard_fixes.py +0 -0
  138. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_keyboard_integration.py +0 -0
  139. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_keyboard_interrupt.py +0 -0
  140. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_keyboard_verification.py +0 -0
  141. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_mcp_command.py +0 -0
  142. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_repl.py +0 -0
  143. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_repl_attribute_fix.py +0 -0
  144. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_repl_startup_message.py +0 -0
  145. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_repl_toolbar.py +0 -0
  146. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/config/__init__.py +0 -0
  147. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/config/test_context.py +0 -0
  148. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/config/test_schema.py +0 -0
  149. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/config/test_settings.py +0 -0
  150. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/conftest.py +0 -0
  151. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/core/__init__.py +0 -0
  152. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/core/test_automatic_compaction.py +0 -0
  153. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/core/test_events.py +0 -0
  154. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/core/test_session.py +0 -0
  155. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/core/test_session_manager.py +0 -0
  156. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/core/test_streaming_tool_calls.py +0 -0
  157. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/e2e/test_tool_fix.py +0 -0
  158. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/empty_message_validation/test_empty_messages.py +0 -0
  159. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/extensions/__init__.py +0 -0
  160. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/extensions/test_base.py +0 -0
  161. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/extensions/test_command.py +0 -0
  162. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/extensions/test_manager.py +0 -0
  163. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/integration/test_context_limits.py +0 -0
  164. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/integration/test_tool_integration.py +0 -0
  165. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/mcp/__init__.py +0 -0
  166. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/mcp/test_client.py +0 -0
  167. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/mcp/test_config.py +0 -0
  168. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/mcp/test_manager.py +0 -0
  169. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/mcp/test_tool.py +0 -0
  170. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/providers/__init__.py +0 -0
  171. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/providers/test_413_error_handling.py +0 -0
  172. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/providers/test_anthropic.py +0 -0
  173. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/providers/test_base.py +0 -0
  174. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/providers/test_deepseek.py +0 -0
  175. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/providers/test_ollama.py +0 -0
  176. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/providers/test_openai_compat.py +0 -0
  177. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/providers/test_registry.py +0 -0
  178. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/skills/test_executor.py +0 -0
  179. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/skills/test_learner.py +0 -0
  180. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/skills/test_markdown_skills.py +0 -0
  181. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/skills/test_models.py +0 -0
  182. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/skills/test_store.py +0 -0
  183. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/skills/test_store_extended.py +0 -0
  184. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/smoke/test_large_file_handling.py +0 -0
  185. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/test_coverage_suite.py +0 -0
  186. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/tools/__init__.py +0 -0
  187. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/tools/test_ask_user_tool.py +0 -0
  188. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/tools/test_base.py +0 -0
  189. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/tools/test_directory_tools.py +0 -0
  190. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/tools/test_file_tools.py +0 -0
  191. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/tools/test_grep_tool.py +0 -0
  192. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/tools/test_plan_mode_enforcement.py +0 -0
  193. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/tools/test_registry.py +0 -0
  194. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/tools/test_shell_tool.py +0 -0
  195. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/tools/test_web_fetch_tool.py +0 -0
  196. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/ui_integration/INTERACTIVE_SESSION_TESTS.md +0 -0
  197. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/ui_integration/__init__.py +0 -0
  198. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/ui_integration/conftest.py +0 -0
  199. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/ui_integration/test_agent.py +0 -0
  200. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/ui_integration/test_compaction_llm.py +0 -0
  201. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/ui_integration/test_events.py +0 -0
  202. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/ui_integration/test_llm.py +0 -0
  203. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/ui_integration/test_mcp.py +0 -0
  204. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/ui_integration/test_plan_mode.py +0 -0
  205. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/ui_integration/test_repl_e2e.py +0 -0
  206. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/ui_integration/test_repl_integration.py +0 -0
  207. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/ui_integration/test_session.py +0 -0
  208. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/ui_integration/test_skills.py +0 -0
  209. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/ui_integration/test_slash_commands.py +0 -0
  210. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/ui_integration/test_tokens_llm.py +0 -0
  211. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/ui_integration/test_tool_calls.py +0 -0
  212. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/ui_integration/test_tool_integration.py +0 -0
  213. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/utils/test_compaction.py +0 -0
  214. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/utils/test_multi_turn_tool_calls.py +0 -0
  215. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/utils/test_summarization.py +0 -0
  216. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/utils/test_tiktoken_integration.py +0 -0
  217. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/utils/test_token_counter_extended.py +0 -0
  218. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/utils/test_tool_sequence_compaction.py +0 -0
  219. {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/utils/test_validation.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: henchman-ai
3
- Version: 0.1.6
3
+ Version: 0.1.7
4
4
  Summary: A model-agnostic AI agent CLI - your AI henchman for the terminal
5
5
  Project-URL: Homepage, https://github.com/MGPowerlytics/henchman-ai
6
6
  Project-URL: Repository, https://github.com/MGPowerlytics/henchman-ai
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "henchman-ai"
7
- version = "0.1.6"
7
+ version = "0.1.7"
8
8
  description = "A model-agnostic AI agent CLI - your AI henchman for the terminal"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -80,16 +80,16 @@ class GlobTool(Tool):
80
80
  # Use a generator approach to avoid loading all files into memory if possible
81
81
  # But glob() returns a generator anyway.
82
82
  matches_iter = base_path.glob(pattern)
83
-
83
+
84
84
  matches = []
85
85
  truncated = False
86
-
86
+
87
87
  try:
88
88
  for _ in range(self.MAX_MATCHES + 1):
89
89
  matches.append(next(matches_iter))
90
90
  except StopIteration:
91
91
  pass
92
-
92
+
93
93
  if len(matches) > self.MAX_MATCHES:
94
94
  truncated = True
95
95
  matches = matches[:self.MAX_MATCHES]
@@ -108,7 +108,7 @@ class GlobTool(Tool):
108
108
  results.append(str(rel_path))
109
109
  except ValueError: # pragma: no cover
110
110
  results.append(str(match))
111
-
111
+
112
112
  if truncated:
113
113
  results.append(f"... Output truncated (limit reached: {self.MAX_MATCHES} matches) ...")
114
114
 
@@ -132,11 +132,8 @@ class GrepTool(Tool):
132
132
  for i, line in enumerate(lines, 1):
133
133
  if regex.search(line):
134
134
  prefix = f"{file_path}:" if len(files) > 1 else ""
135
- if line_numbers:
136
- match_str = f"{prefix}{i}:{line}"
137
- else:
138
- match_str = f"{prefix}{line}"
139
-
135
+ match_str = f"{prefix}{i}:{line}" if line_numbers else f"{prefix}{line}"
136
+
140
137
  results.append(match_str)
141
138
  total_chars += len(match_str) + 1 # +1 for newline
142
139
 
@@ -86,24 +86,24 @@ class LsTool(Tool):
86
86
  # List directory contents
87
87
  entries = []
88
88
  truncated = False
89
-
89
+
90
90
  # Use iterdir() which returns an iterator
91
91
  iterator = target.iterdir()
92
- # We can't sort immediately if we want to limit processing,
92
+ # We can't sort immediately if we want to limit processing,
93
93
  # but for consistent output on small dirs, sorting is better.
94
94
  # So we collect up to limit + 1
95
-
95
+
96
96
  all_items = []
97
97
  try:
98
98
  for _ in range(self.MAX_ITEMS + 1):
99
99
  all_items.append(next(iterator))
100
100
  except StopIteration:
101
101
  pass
102
-
102
+
103
103
  if len(all_items) > self.MAX_ITEMS:
104
104
  truncated = True
105
105
  all_items = all_items[:self.MAX_ITEMS]
106
-
106
+
107
107
  # Sort the collected items
108
108
  all_items.sort(key=lambda p: p.name)
109
109
 
@@ -117,7 +117,7 @@ class LsTool(Tool):
117
117
  entries.append(f"{item.name}/")
118
118
  else:
119
119
  entries.append(item.name)
120
-
120
+
121
121
  if truncated:
122
122
  entries.append(f"... Output truncated (limit reached: {self.MAX_ITEMS} items) ...")
123
123
 
@@ -114,7 +114,7 @@ class ShellTool(Tool):
114
114
  output_parts.append(stderr_text)
115
115
 
116
116
  output = "\n".join(output_parts)
117
-
117
+
118
118
  # Truncate if too long
119
119
  if len(output) > self.MAX_OUTPUT_CHARS:
120
120
  output = output[:self.MAX_OUTPUT_CHARS] + f"\n... (output truncated after {self.MAX_OUTPUT_CHARS} chars)"
@@ -68,9 +68,6 @@ class ContextCompactor:
68
68
 
69
69
  Preserves atomic sequences, especially tool call sequences.
70
70
  """
71
-
72
- # Safety limit for individual message size
73
- MAX_MESSAGE_CHARS = 100_000
74
71
 
75
72
  def __init__(self, max_tokens: int = 8000) -> None:
76
73
  """Initialize compactor.
@@ -81,10 +78,10 @@ class ContextCompactor:
81
78
  self.max_tokens = max_tokens
82
79
 
83
80
  def enforce_safety_limits(self, messages: list[Message]) -> list[Message]:
84
- """Enforce hard safety limits on individual message content size.
81
+ """Enforce limits on individual message size using tokens.
85
82
 
86
83
  This prevents context overflow from individual massive messages
87
- that might slip through tool-specific limits.
84
+ by truncating them to fit within the context window.
88
85
 
89
86
  Args:
90
87
  messages: List of messages to check.
@@ -93,11 +90,33 @@ class ContextCompactor:
93
90
  List of messages with content limits enforced.
94
91
  """
95
92
  safe_messages = []
93
+ # Reserve tokens for overhead/other messages.
94
+ # We use 75% of max_tokens to allow for message overhead, system prompts,
95
+ # and the truncation suffix itself.
96
+ limit = int(self.max_tokens * 0.75)
97
+
96
98
  for msg in messages:
97
- # Check content length
98
- if msg.content and len(msg.content) > self.MAX_MESSAGE_CHARS:
99
- # Create a new message with truncated content
100
- new_content = msg.content[:self.MAX_MESSAGE_CHARS] + f"\n... (truncated by safety limit: > {self.MAX_MESSAGE_CHARS} chars)"
99
+ if not msg.content:
100
+ safe_messages.append(msg)
101
+ continue
102
+
103
+ # Quick character check optimization:
104
+ # If chars < limit, tokens are definitely < limit (1 token >= 1 char usually)
105
+ # Actually, 1 token ~ 4 chars. So if chars < limit, it's definitely safe?
106
+ # No, if chars < limit, tokens could be anything.
107
+ # But if chars < limit (tokens), then tokens < limit is guaranteed since token count <= char count?
108
+ # Tiktoken: "hello" (5 chars) -> 1 token. " " (1 char) -> 1 token.
109
+ # Generally token count < char count.
110
+ # So if len(msg.content) < limit, we are safe.
111
+ if len(msg.content) < limit:
112
+ safe_messages.append(msg)
113
+ continue
114
+
115
+ # Check token count
116
+ if TokenCounter.count_text(msg.content) > limit:
117
+ # Truncate
118
+ truncated_content = TokenCounter.truncate_text(msg.content, limit)
119
+ new_content = truncated_content + f"\n... (truncated by safety limit: > {limit} tokens)"
101
120
 
102
121
  # Create copy with modified content
103
122
  safe_msg = Message(
@@ -111,7 +130,6 @@ class ContextCompactor:
111
130
  safe_messages.append(msg)
112
131
 
113
132
  return safe_messages
114
-
115
133
  def _group_into_sequences(self, messages: list[Message]) -> list[MessageSequence]:
116
134
  """Group messages into atomic sequences that must be kept together.
117
135
 
@@ -183,7 +201,7 @@ class ContextCompactor:
183
201
  """
184
202
  if not messages: # pragma: no cover
185
203
  return []
186
-
204
+
187
205
  # First, enforce safety limits on individual messages
188
206
  # This prevents massive messages from breaking the token counter or API
189
207
  messages = self.enforce_safety_limits(messages)
@@ -461,7 +479,7 @@ async def compact_with_summarization(
461
479
  return result
462
480
 
463
481
  # Identify dropped messages for summarization
464
- kept_set = set(id(m) for m in result.messages)
482
+ kept_set = {id(m) for m in result.messages}
465
483
  dropped_messages = [m for m in messages if id(m) not in kept_set]
466
484
 
467
485
  # Attempt summarization if enabled and we have a provider
@@ -110,6 +110,30 @@ class TokenCounter:
110
110
  encoding = cls._get_encoding(model)
111
111
  return len(encoding.encode(text))
112
112
 
113
+ @classmethod
114
+ def truncate_text(cls, text: str, max_tokens: int, model: str | None = None) -> str:
115
+ """Truncate text to a maximum number of tokens.
116
+
117
+ Args:
118
+ text: The text to truncate.
119
+ max_tokens: Maximum number of tokens allowed.
120
+ model: Optional model name.
121
+
122
+ Returns:
123
+ The truncated text.
124
+ """
125
+ if not text:
126
+ return ""
127
+
128
+ encoding = cls._get_encoding(model)
129
+ tokens = encoding.encode(text)
130
+ if len(tokens) <= max_tokens:
131
+ return text
132
+
133
+ # Decode the truncated tokens
134
+ # Note: We don't handle partial unicode bytes here as tiktoken handles text -> tokens -> text
135
+ return encoding.decode(tokens[:max_tokens])
136
+
113
137
  @classmethod
114
138
  def count_messages(cls, messages: list[Message], model: str | None = None) -> int:
115
139
  """Count tokens in a list of messages.
@@ -1,6 +1,6 @@
1
1
  """Version information for Henchman-AI."""
2
2
 
3
- VERSION_TUPLE = (0, 1, 6)
3
+ VERSION_TUPLE = (0, 1, 7)
4
4
  VERSION = ".".join(str(v) for v in VERSION_TUPLE)
5
5
 
6
6
  __all__ = ["VERSION", "VERSION_TUPLE"]
@@ -1,29 +1,27 @@
1
1
 
2
- import asyncio
2
+
3
3
  import pytest
4
- from pathlib import Path
5
- import os
6
- import shutil
7
4
 
8
- from henchman.tools.builtins.shell import ShellTool
5
+ from henchman.providers.base import Message, ToolCall
9
6
  from henchman.tools.builtins.glob_tool import GlobTool
7
+ from henchman.tools.builtins.shell import ShellTool
10
8
  from henchman.utils.compaction import ContextCompactor
11
- from henchman.providers.base import Message, ToolCall
9
+
12
10
 
13
11
  @pytest.mark.asyncio
14
12
  class TestContextSafety:
15
-
13
+
16
14
  async def test_shell_tool_truncation(self):
17
15
  """Test that ShellTool truncates excessive output."""
18
16
  tool = ShellTool()
19
17
  limit = tool.MAX_OUTPUT_CHARS
20
-
18
+
21
19
  # Create a command that produces massive output
22
20
  # Using python -c to generate it
23
21
  cmd = f"python3 -c 'print(\"a\" * {limit + 1000})'"
24
-
22
+
25
23
  result = await tool.execute(command=cmd)
26
-
24
+
27
25
  assert result.success
28
26
  assert len(result.content) < limit + 200 # Allow for "truncated" message
29
27
  assert "output truncated" in result.content
@@ -33,33 +31,36 @@ class TestContextSafety:
33
31
  """Test that GlobTool truncates matching list."""
34
32
  tool = GlobTool()
35
33
  limit = tool.MAX_MATCHES
36
-
34
+
37
35
  # Create more files than the limit
38
36
  test_dir = tmp_path / "glob_test"
39
37
  test_dir.mkdir()
40
-
38
+
41
39
  for i in range(limit + 50):
42
40
  (test_dir / f"test_{i}.txt").touch()
43
-
41
+
44
42
  result = await tool.execute(pattern="*.txt", path=str(test_dir))
45
-
43
+
46
44
  assert result.success
47
45
  assert "Output truncated" in result.content
48
46
  assert f"limit reached: {limit} matches" in result.content
49
-
47
+
50
48
  # Count lines (excluding the truncation message)
51
49
  lines = result.content.splitlines()
52
50
  # The last line is the truncation message
53
- assert len(lines) == limit + 1
51
+ assert len(lines) == limit + 1
54
52
  assert lines[-1].startswith("... Output truncated")
55
53
 
56
54
  def test_compactor_safety_limit(self):
57
55
  """Test that ContextCompactor enforces limits on individual messages."""
58
- compactor = ContextCompactor(max_tokens=100000)
59
- limit = compactor.MAX_MESSAGE_CHARS
60
-
61
- # Create a message exceeding the limit
62
- huge_content = "a" * (limit + 5000)
56
+ # Use token limit large enough to fit overhead but small enough to test truncation
57
+ limit_tokens = 300
58
+ compactor = ContextCompactor(max_tokens=limit_tokens)
59
+
60
+ # Limit will be 0.9 * 300 = 270 tokens
61
+ # Create a message exceeding the limit
62
+ # "word " is 1 token. We want > 270 tokens.
63
+ huge_content = "word " * 500
63
64
  messages = [
64
65
  Message(role="system", content="System prompt"),
65
66
  Message(role="user", content="Here is a huge message:"),
@@ -70,9 +71,11 @@ class TestContextSafety:
70
71
  compacted = compactor.compact(messages)
71
72
 
72
73
  # Check that the huge message was truncated
74
+ # With max_tokens=300, limit is 270.
75
+ # Total tokens should be ~290 < 300. So all messages should be kept.
76
+ assert len(compacted) == 3
73
77
  huge_msg = compacted[2]
74
78
  assert len(huge_msg.content) < len(huge_content)
75
- assert len(huge_msg.content) <= limit + 200 # approximate buffer for truncation msg
76
79
  assert "truncated by safety limit" in huge_msg.content
77
80
 
78
81
  # Check other messages are untouched
@@ -81,12 +84,11 @@ class TestContextSafety:
81
84
 
82
85
  async def test_compactor_safety_limit_preserves_tool_calls(self):
83
86
  """Test that safety limit preserves tool call structure even if content is truncated."""
84
- # Use large max_tokens so compaction logic doesn't drop the message purely due to size
85
- # We want to test that enforce_safety_limits runs and preserves tool_calls
86
- compactor = ContextCompactor(max_tokens=200000)
87
- limit = compactor.MAX_MESSAGE_CHARS
87
+ # Use token limit large enough to fit overhead
88
+ limit_tokens = 300
89
+ compactor = ContextCompactor(max_tokens=limit_tokens)
88
90
 
89
- huge_content = "b" * (limit + 1000)
91
+ huge_content = "word " * 500
90
92
 
91
93
  tool_call = ToolCall(id="call_1", name="test_tool", arguments={})
92
94
 
@@ -21,7 +21,7 @@ def test_main_module_imports() -> None:
21
21
  text=True,
22
22
  )
23
23
  assert result.returncode == 0
24
- assert "0.1.6" in result.stdout
24
+ assert "0.1.7" in result.stdout
25
25
 
26
26
  def test_python_m_henchman_help() -> None:
27
27
  """Test that python -m henchman --help works."""
@@ -13,7 +13,7 @@ def test_version_string_format() -> None:
13
13
 
14
14
  def test_version_tuple() -> None:
15
15
  """Test that version tuple matches version string."""
16
- assert VERSION_TUPLE == (0, 1, 6)
16
+ assert VERSION_TUPLE == (0, 1, 7)
17
17
  assert ".".join(str(v) for v in VERSION_TUPLE) == VERSION
18
18
 
19
19
 
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes