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.
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/PKG-INFO +1 -1
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/pyproject.toml +1 -1
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/tools/builtins/glob_tool.py +4 -4
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/tools/builtins/grep.py +2 -5
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/tools/builtins/ls.py +6 -6
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/tools/builtins/shell.py +1 -1
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/utils/compaction.py +30 -12
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/utils/tokens.py +24 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/version.py +1 -1
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/e2e/test_context_safety.py +29 -27
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/test_main.py +1 -1
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/test_version.py +1 -1
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/.github/copilot-instructions.md +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/.github/workflows/ci.yml +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/.github/workflows/publish.yml +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/.gitignore +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/400_error.md +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/400_error_final_report.md +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/400_error_fix_report.md +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/ATTRIBUTE_ERROR_FIX_SUMMARY.md +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/CHANGELOG.md +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/CI_100_PERCENT_PASS_RATE_ANALYSIS.md +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/CI_FIX_ACTION_PLAN.md +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/CI_FIX_PROGRESS_SUMMARY.md +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/CI_PIPELINE_COMPREHENSIVE_ANALYSIS.md +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/FUNCTIONAL_TEST_IMPLEMENTATION_SUMMARY.md +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/IMPLEMENTATION_PLAN.md +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/INTEGRATION_TESTING.md +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/INTEGRATION_TESTING_PROGRESS_SUMMARY.md +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/INTEGRATION_TESTING_TASK_COMPLETION.md +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/KEYBOARD_INTERRUPT_FIXES_SUMMARY.md +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/LICENSE +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/README.md +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/TASK_COMPLETION_SUMMARY.md +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/UI_INTEGRATION_TESTING_SUMMARY.md +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/UI_TOOL_CALLS_SUMMARY.md +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/analyze_400_error.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/compaction_bug_analysis.md +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/debug_compaction.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/detailed_400_analysis.md +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/docs/api.md +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/docs/configuration.md +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/docs/extensions.md +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/docs/getting-started.md +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/docs/index.md +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/docs/mcp.md +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/docs/providers.md +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/docs/tools.md +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/fix_repl.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/fix_repl_simple.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/mkdocs.yml +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/replace_method.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/reproduce_400_error.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/run_interactive_tests.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/scripts/ci.sh +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/__init__.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/__main__.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/cli/__init__.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/cli/app.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/cli/commands/__init__.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/cli/commands/builtins.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/cli/commands/chat.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/cli/commands/extensions.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/cli/commands/mcp.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/cli/commands/plan.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/cli/commands/skill.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/cli/console.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/cli/input.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/cli/json_output.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/cli/prompts.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/cli/repl.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/cli/repl.py.backup +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/cli/repl.py.backup2 +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/config/__init__.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/config/context.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/config/schema.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/config/settings.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/core/__init__.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/core/agent.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/core/agent.py.backup +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/core/events.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/core/session.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/extensions/__init__.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/extensions/base.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/extensions/manager.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/mcp/__init__.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/mcp/client.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/mcp/config.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/mcp/manager.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/mcp/tool.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/providers/__init__.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/providers/anthropic.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/providers/base.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/providers/deepseek.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/providers/ollama.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/providers/openai_compat.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/providers/openai_compat.py.backup +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/providers/registry.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/skills/__init__.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/skills/executor.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/skills/learner.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/skills/models.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/skills/store.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/tools/__init__.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/tools/base.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/tools/builtins/__init__.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/tools/builtins/ask_user.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/tools/builtins/file_edit.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/tools/builtins/file_read.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/tools/builtins/file_write.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/tools/builtins/web_fetch.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/tools/registry.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/utils/__init__.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/src/henchman/utils/validation.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/test_compaction.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/test_compaction_fix.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/test_fixes.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/test_output.txt +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/test_run.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/__init__.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/__init__.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/commands/test_plan.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/commands/test_skill.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/commands/test_skill_extended.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_app.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_app_extended.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_builtins.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_chat_command.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_cli_smoke.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_commands.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_commands_repro.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_console.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_enhanced_tool_display.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_input.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_input_bindings.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_json_output.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_keyboard_fixes.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_keyboard_integration.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_keyboard_interrupt.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_keyboard_verification.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_mcp_command.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_repl.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_repl_attribute_fix.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_repl_startup_message.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/cli/test_repl_toolbar.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/config/__init__.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/config/test_context.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/config/test_schema.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/config/test_settings.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/conftest.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/core/__init__.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/core/test_automatic_compaction.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/core/test_events.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/core/test_session.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/core/test_session_manager.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/core/test_streaming_tool_calls.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/e2e/test_tool_fix.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/empty_message_validation/test_empty_messages.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/extensions/__init__.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/extensions/test_base.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/extensions/test_command.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/extensions/test_manager.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/integration/test_context_limits.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/integration/test_tool_integration.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/mcp/__init__.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/mcp/test_client.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/mcp/test_config.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/mcp/test_manager.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/mcp/test_tool.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/providers/__init__.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/providers/test_413_error_handling.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/providers/test_anthropic.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/providers/test_base.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/providers/test_deepseek.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/providers/test_ollama.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/providers/test_openai_compat.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/providers/test_registry.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/skills/test_executor.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/skills/test_learner.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/skills/test_markdown_skills.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/skills/test_models.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/skills/test_store.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/skills/test_store_extended.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/smoke/test_large_file_handling.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/test_coverage_suite.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/tools/__init__.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/tools/test_ask_user_tool.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/tools/test_base.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/tools/test_directory_tools.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/tools/test_file_tools.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/tools/test_grep_tool.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/tools/test_plan_mode_enforcement.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/tools/test_registry.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/tools/test_shell_tool.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/tools/test_web_fetch_tool.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/ui_integration/INTERACTIVE_SESSION_TESTS.md +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/ui_integration/__init__.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/ui_integration/conftest.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/ui_integration/test_agent.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/ui_integration/test_compaction_llm.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/ui_integration/test_events.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/ui_integration/test_llm.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/ui_integration/test_mcp.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/ui_integration/test_plan_mode.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/ui_integration/test_repl_e2e.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/ui_integration/test_repl_integration.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/ui_integration/test_session.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/ui_integration/test_skills.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/ui_integration/test_slash_commands.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/ui_integration/test_tokens_llm.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/ui_integration/test_tool_calls.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/ui_integration/test_tool_integration.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/utils/test_compaction.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/utils/test_multi_turn_tool_calls.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/utils/test_summarization.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/utils/test_tiktoken_integration.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/utils/test_token_counter_extended.py +0 -0
- {henchman_ai-0.1.6 → henchman_ai-0.1.7}/tests/utils/test_tool_sequence_compaction.py +0 -0
- {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.
|
|
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
|
|
@@ -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
|
-
|
|
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
|
|
81
|
+
"""Enforce limits on individual message size using tokens.
|
|
85
82
|
|
|
86
83
|
This prevents context overflow from individual massive messages
|
|
87
|
-
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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 =
|
|
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,29 +1,27 @@
|
|
|
1
1
|
|
|
2
|
-
|
|
2
|
+
|
|
3
3
|
import pytest
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
import os
|
|
6
|
-
import shutil
|
|
7
4
|
|
|
8
|
-
from henchman.
|
|
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
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
|
85
|
-
|
|
86
|
-
compactor = ContextCompactor(max_tokens=
|
|
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 = "
|
|
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.
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
File without changes
|