auto-coder 1.0.0__py3-none-any.whl → 2.0.0__py3-none-any.whl
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.
Potentially problematic release.
This version of auto-coder might be problematic. Click here for more details.
- auto_coder-2.0.0.dist-info/LICENSE +158 -0
- auto_coder-2.0.0.dist-info/METADATA +558 -0
- auto_coder-2.0.0.dist-info/RECORD +795 -0
- {auto_coder-1.0.0.dist-info → auto_coder-2.0.0.dist-info}/WHEEL +1 -1
- {auto_coder-1.0.0.dist-info → auto_coder-2.0.0.dist-info}/entry_points.txt +3 -3
- autocoder/__init__.py +31 -0
- autocoder/agent/auto_filegroup.py +32 -13
- autocoder/agent/auto_learn_from_commit.py +9 -1
- autocoder/agent/base_agentic/__init__.py +3 -0
- autocoder/agent/base_agentic/agent_hub.py +1 -1
- autocoder/agent/base_agentic/base_agent.py +235 -136
- autocoder/agent/base_agentic/default_tools.py +119 -118
- autocoder/agent/base_agentic/test_base_agent.py +1 -1
- autocoder/agent/base_agentic/tool_registry.py +32 -20
- autocoder/agent/base_agentic/tools/read_file_tool_resolver.py +24 -3
- autocoder/agent/base_agentic/tools/write_to_file_tool_resolver.py +24 -11
- autocoder/agent/base_agentic/types.py +42 -0
- autocoder/agent/entry_command_agent/chat.py +73 -59
- autocoder/auto_coder.py +31 -40
- autocoder/auto_coder_rag.py +11 -1084
- autocoder/auto_coder_runner.py +970 -2345
- autocoder/auto_coder_terminal.py +26 -0
- autocoder/auto_coder_terminal_v3.py +190 -0
- autocoder/chat/conf_command.py +224 -124
- autocoder/chat/models_command.py +361 -299
- autocoder/chat/rules_command.py +79 -31
- autocoder/chat_auto_coder.py +988 -398
- autocoder/chat_auto_coder_lang.py +23 -732
- autocoder/commands/auto_command.py +25 -8
- autocoder/commands/auto_web.py +1 -1
- autocoder/commands/tools.py +44 -44
- autocoder/common/__init__.py +150 -128
- autocoder/common/ac_style_command_parser/__init__.py +39 -2
- autocoder/common/ac_style_command_parser/config.py +422 -0
- autocoder/common/ac_style_command_parser/parser.py +292 -78
- autocoder/common/ac_style_command_parser/test_parser.py +241 -16
- autocoder/common/ac_style_command_parser/test_typed_parser.py +342 -0
- autocoder/common/ac_style_command_parser/typed_parser.py +653 -0
- autocoder/common/action_yml_file_manager.py +25 -13
- autocoder/common/agent_events/__init__.py +52 -0
- autocoder/common/agent_events/agent_event_emitter.py +193 -0
- autocoder/common/agent_events/event_factory.py +177 -0
- autocoder/common/agent_events/examples.py +307 -0
- autocoder/common/agent_events/types.py +113 -0
- autocoder/common/agent_events/utils.py +68 -0
- autocoder/common/agent_hooks/__init__.py +44 -0
- autocoder/common/agent_hooks/examples.py +582 -0
- autocoder/common/agent_hooks/hook_executor.py +217 -0
- autocoder/common/agent_hooks/hook_manager.py +288 -0
- autocoder/common/agent_hooks/types.py +133 -0
- autocoder/common/agent_hooks/utils.py +99 -0
- autocoder/common/agent_query_queue/queue_executor.py +324 -0
- autocoder/common/agent_query_queue/queue_manager.py +325 -0
- autocoder/common/agents/__init__.py +11 -0
- autocoder/common/agents/agent_manager.py +323 -0
- autocoder/common/agents/agent_parser.py +189 -0
- autocoder/common/agents/example_usage.py +344 -0
- autocoder/common/agents/integration_example.py +330 -0
- autocoder/common/agents/test_agent_parser.py +545 -0
- autocoder/common/async_utils.py +101 -0
- autocoder/common/auto_coder_lang.py +23 -972
- autocoder/common/autocoderargs_parser/__init__.py +14 -0
- autocoder/common/autocoderargs_parser/parser.py +184 -0
- autocoder/common/autocoderargs_parser/tests/__init__.py +1 -0
- autocoder/common/autocoderargs_parser/tests/test_args_parser.py +235 -0
- autocoder/common/autocoderargs_parser/tests/test_token_parser.py +195 -0
- autocoder/common/autocoderargs_parser/token_parser.py +290 -0
- autocoder/common/buildin_tokenizer.py +2 -4
- autocoder/common/code_auto_generate.py +149 -74
- autocoder/common/code_auto_generate_diff.py +163 -70
- autocoder/common/code_auto_generate_editblock.py +179 -89
- autocoder/common/code_auto_generate_strict_diff.py +167 -72
- autocoder/common/code_auto_merge_editblock.py +13 -6
- autocoder/common/code_modification_ranker.py +1 -1
- autocoder/common/command_completer.py +3 -3
- autocoder/common/command_file_manager/manager.py +183 -47
- autocoder/common/command_file_manager/test_command_file_manager.py +507 -0
- autocoder/common/command_templates.py +1 -1
- autocoder/common/conf_utils.py +2 -4
- autocoder/common/conversations/config.py +11 -3
- autocoder/common/conversations/get_conversation_manager.py +100 -2
- autocoder/common/conversations/llm_stats_models.py +264 -0
- autocoder/common/conversations/manager.py +112 -28
- autocoder/common/conversations/models.py +16 -2
- autocoder/common/conversations/storage/index_manager.py +134 -10
- autocoder/common/core_config/__init__.py +63 -0
- autocoder/common/core_config/agentic_mode_manager.py +109 -0
- autocoder/common/core_config/base_manager.py +123 -0
- autocoder/common/core_config/compatibility.py +151 -0
- autocoder/common/core_config/config_manager.py +156 -0
- autocoder/common/core_config/conversation_manager.py +31 -0
- autocoder/common/core_config/exclude_manager.py +72 -0
- autocoder/common/core_config/file_manager.py +177 -0
- autocoder/common/core_config/human_as_model_manager.py +129 -0
- autocoder/common/core_config/lib_manager.py +54 -0
- autocoder/common/core_config/main_manager.py +81 -0
- autocoder/common/core_config/mode_manager.py +126 -0
- autocoder/common/core_config/models.py +70 -0
- autocoder/common/core_config/test_memory_manager.py +1056 -0
- autocoder/common/env_manager.py +282 -0
- autocoder/common/env_manager_usage_example.py +211 -0
- autocoder/common/file_checkpoint/conversation_checkpoint.py +19 -19
- autocoder/common/file_checkpoint/manager.py +264 -48
- autocoder/common/file_checkpoint/test_backup.py +1 -18
- autocoder/common/file_checkpoint/test_manager.py +270 -1
- autocoder/common/file_checkpoint/test_store.py +1 -17
- autocoder/common/file_handler/__init__.py +23 -0
- autocoder/common/file_handler/active_context_handler.py +159 -0
- autocoder/common/file_handler/add_files_handler.py +409 -0
- autocoder/common/file_handler/chat_handler.py +180 -0
- autocoder/common/file_handler/coding_handler.py +401 -0
- autocoder/common/file_handler/commit_handler.py +200 -0
- autocoder/common/file_handler/lib_handler.py +156 -0
- autocoder/common/file_handler/list_files_handler.py +111 -0
- autocoder/common/file_handler/mcp_handler.py +268 -0
- autocoder/common/file_handler/models_handler.py +493 -0
- autocoder/common/file_handler/remove_files_handler.py +172 -0
- autocoder/common/git_utils.py +44 -8
- autocoder/common/global_cancel.py +15 -6
- autocoder/common/ignorefiles/test_ignore_file_utils.py +1 -1
- autocoder/common/international/__init__.py +31 -0
- autocoder/common/international/demo_international.py +92 -0
- autocoder/common/international/message_manager.py +157 -0
- autocoder/common/international/messages/__init__.py +56 -0
- autocoder/common/international/messages/async_command_messages.py +507 -0
- autocoder/common/international/messages/auto_coder_messages.py +2208 -0
- autocoder/common/international/messages/chat_auto_coder_messages.py +1547 -0
- autocoder/common/international/messages/command_help_messages.py +986 -0
- autocoder/common/international/messages/conversation_command_messages.py +191 -0
- autocoder/common/international/messages/git_helper_plugin_messages.py +159 -0
- autocoder/common/international/messages/queue_command_messages.py +751 -0
- autocoder/common/international/messages/rules_command_messages.py +77 -0
- autocoder/common/international/messages/sdk_messages.py +1707 -0
- autocoder/common/international/messages/token_helper_plugin_messages.py +361 -0
- autocoder/common/international/messages/tool_display_messages.py +1212 -0
- autocoder/common/international/messages/workflow_exception_messages.py +473 -0
- autocoder/common/international/test_international.py +612 -0
- autocoder/common/linter_core/__init__.py +28 -0
- autocoder/common/linter_core/base_linter.py +61 -0
- autocoder/common/linter_core/config_loader.py +271 -0
- autocoder/common/linter_core/formatters/__init__.py +0 -0
- autocoder/common/linter_core/formatters/base_formatter.py +38 -0
- autocoder/common/linter_core/formatters/raw_formatter.py +17 -0
- autocoder/common/linter_core/linter.py +166 -0
- autocoder/common/linter_core/linter_factory.py +216 -0
- autocoder/common/linter_core/linter_manager.py +333 -0
- autocoder/common/linter_core/linters/__init__.py +9 -0
- autocoder/common/linter_core/linters/java_linter.py +342 -0
- autocoder/common/linter_core/linters/python_linter.py +115 -0
- autocoder/common/linter_core/linters/typescript_linter.py +119 -0
- autocoder/common/linter_core/models/__init__.py +7 -0
- autocoder/common/linter_core/models/lint_result.py +91 -0
- autocoder/common/linter_core/models.py +33 -0
- autocoder/common/linter_core/tests/__init__.py +3 -0
- autocoder/common/linter_core/tests/test_config_loader.py +323 -0
- autocoder/common/linter_core/tests/test_config_loading.py +308 -0
- autocoder/common/linter_core/tests/test_factory_manager.py +234 -0
- autocoder/common/linter_core/tests/test_formatters.py +147 -0
- autocoder/common/linter_core/tests/test_integration.py +317 -0
- autocoder/common/linter_core/tests/test_java_linter.py +496 -0
- autocoder/common/linter_core/tests/test_linters.py +265 -0
- autocoder/common/linter_core/tests/test_models.py +81 -0
- autocoder/common/linter_core/tests/verify_config_loading.py +296 -0
- autocoder/common/linter_core/tests/verify_fixes.py +183 -0
- autocoder/common/llm_friendly_package/__init__.py +31 -0
- autocoder/common/llm_friendly_package/base_manager.py +102 -0
- autocoder/common/llm_friendly_package/docs_manager.py +121 -0
- autocoder/common/llm_friendly_package/library_manager.py +171 -0
- autocoder/common/{llm_friendly_package.py → llm_friendly_package/main_manager.py} +204 -231
- autocoder/common/llm_friendly_package/models.py +40 -0
- autocoder/common/llm_friendly_package/test_llm_friendly_package.py +536 -0
- autocoder/common/llms/__init__.py +15 -0
- autocoder/common/llms/demo_error_handling.py +85 -0
- autocoder/common/llms/factory.py +142 -0
- autocoder/common/llms/manager.py +264 -0
- autocoder/common/llms/pricing.py +121 -0
- autocoder/common/llms/registry.py +288 -0
- autocoder/common/llms/schema.py +77 -0
- autocoder/common/llms/simple_demo.py +45 -0
- autocoder/common/llms/test_quick_model.py +116 -0
- autocoder/common/llms/test_remove_functionality.py +182 -0
- autocoder/common/llms/tests/__init__.py +1 -0
- autocoder/common/llms/tests/test_manager.py +330 -0
- autocoder/common/llms/tests/test_registry.py +364 -0
- autocoder/common/mcp_tools/__init__.py +62 -0
- autocoder/common/{mcp_tools.py → mcp_tools/executor.py} +49 -40
- autocoder/common/{mcp_hub.py → mcp_tools/hub.py} +42 -68
- autocoder/common/{mcp_server_install.py → mcp_tools/installer.py} +16 -28
- autocoder/common/{mcp_server.py → mcp_tools/server.py} +176 -48
- autocoder/common/mcp_tools/test_keyboard_interrupt.py +93 -0
- autocoder/common/mcp_tools/test_mcp_tools.py +391 -0
- autocoder/common/{mcp_server_types.py → mcp_tools/types.py} +121 -48
- autocoder/common/mcp_tools/verify_functionality.py +202 -0
- autocoder/common/model_speed_tester.py +32 -26
- autocoder/common/priority_directory_finder/__init__.py +142 -0
- autocoder/common/priority_directory_finder/examples.py +230 -0
- autocoder/common/priority_directory_finder/finder.py +283 -0
- autocoder/common/priority_directory_finder/models.py +236 -0
- autocoder/common/priority_directory_finder/test_priority_directory_finder.py +431 -0
- autocoder/common/project_scanner/__init__.py +18 -0
- autocoder/common/project_scanner/compat.py +77 -0
- autocoder/common/project_scanner/scanner.py +436 -0
- autocoder/common/project_tracker/__init__.py +27 -0
- autocoder/common/project_tracker/api.py +228 -0
- autocoder/common/project_tracker/demo.py +272 -0
- autocoder/common/project_tracker/tracker.py +487 -0
- autocoder/common/project_tracker/types.py +53 -0
- autocoder/common/pruner/__init__.py +67 -0
- autocoder/common/pruner/agentic_conversation_pruner.py +651 -102
- autocoder/common/pruner/conversation_message_ids_api.py +386 -0
- autocoder/common/pruner/conversation_message_ids_manager.py +347 -0
- autocoder/common/pruner/conversation_message_ids_pruner.py +473 -0
- autocoder/common/pruner/conversation_normalizer.py +347 -0
- autocoder/common/pruner/conversation_pruner.py +26 -6
- autocoder/common/pruner/test_agentic_conversation_pruner.py +554 -112
- autocoder/common/pruner/test_conversation_normalizer.py +502 -0
- autocoder/common/pruner/test_tool_content_detector.py +324 -0
- autocoder/common/pruner/tool_content_detector.py +227 -0
- autocoder/common/pruner/tools/__init__.py +18 -0
- autocoder/common/pruner/tools/query_message_ids.py +264 -0
- autocoder/common/pruner/tools/test_agentic_pruning_logic.py +432 -0
- autocoder/common/pruner/tools/test_message_ids_pruning_only.py +192 -0
- autocoder/common/pull_requests/__init__.py +9 -1
- autocoder/common/pull_requests/utils.py +122 -1
- autocoder/common/rag_manager/rag_manager.py +36 -40
- autocoder/common/rulefiles/__init__.py +53 -1
- autocoder/common/rulefiles/api.py +250 -0
- autocoder/common/rulefiles/core/__init__.py +14 -0
- autocoder/common/rulefiles/core/manager.py +241 -0
- autocoder/common/rulefiles/core/selector.py +805 -0
- autocoder/common/rulefiles/models/__init__.py +20 -0
- autocoder/common/rulefiles/models/index.py +16 -0
- autocoder/common/rulefiles/models/init_rule.py +18 -0
- autocoder/common/rulefiles/models/rule_file.py +18 -0
- autocoder/common/rulefiles/models/rule_relevance.py +14 -0
- autocoder/common/rulefiles/models/summary.py +16 -0
- autocoder/common/rulefiles/test_rulefiles.py +776 -0
- autocoder/common/rulefiles/utils/__init__.py +34 -0
- autocoder/common/rulefiles/utils/monitor.py +86 -0
- autocoder/common/rulefiles/utils/parser.py +230 -0
- autocoder/common/save_formatted_log.py +67 -10
- autocoder/common/search_replace.py +8 -1
- autocoder/common/search_replace_patch/__init__.py +24 -0
- autocoder/common/search_replace_patch/base.py +115 -0
- autocoder/common/search_replace_patch/manager.py +248 -0
- autocoder/common/search_replace_patch/patch_replacer.py +304 -0
- autocoder/common/search_replace_patch/similarity_replacer.py +306 -0
- autocoder/common/search_replace_patch/string_replacer.py +181 -0
- autocoder/common/search_replace_patch/tests/__init__.py +3 -0
- autocoder/common/search_replace_patch/tests/run_tests.py +126 -0
- autocoder/common/search_replace_patch/tests/test_base.py +188 -0
- autocoder/common/search_replace_patch/tests/test_empty_line_insert.py +233 -0
- autocoder/common/search_replace_patch/tests/test_integration.py +389 -0
- autocoder/common/search_replace_patch/tests/test_manager.py +351 -0
- autocoder/common/search_replace_patch/tests/test_patch_replacer.py +316 -0
- autocoder/common/search_replace_patch/tests/test_regex_replacer.py +306 -0
- autocoder/common/search_replace_patch/tests/test_similarity_replacer.py +384 -0
- autocoder/common/shell_commands/__init__.py +197 -0
- autocoder/common/shell_commands/background_process_notifier.py +346 -0
- autocoder/common/shell_commands/command_executor.py +1127 -0
- autocoder/common/shell_commands/error_recovery.py +541 -0
- autocoder/common/shell_commands/exceptions.py +120 -0
- autocoder/common/shell_commands/interactive_executor.py +476 -0
- autocoder/common/shell_commands/interactive_pexpect_process.py +623 -0
- autocoder/common/shell_commands/interactive_process.py +744 -0
- autocoder/common/shell_commands/interactive_session_manager.py +1014 -0
- autocoder/common/shell_commands/monitoring.py +529 -0
- autocoder/common/shell_commands/process_cleanup.py +386 -0
- autocoder/common/shell_commands/process_manager.py +606 -0
- autocoder/common/shell_commands/test_interactive_pexpect_process.py +281 -0
- autocoder/common/shell_commands/tests/__init__.py +6 -0
- autocoder/common/shell_commands/tests/conftest.py +118 -0
- autocoder/common/shell_commands/tests/test_background_process_notifier.py +703 -0
- autocoder/common/shell_commands/tests/test_command_executor.py +448 -0
- autocoder/common/shell_commands/tests/test_error_recovery.py +305 -0
- autocoder/common/shell_commands/tests/test_exceptions.py +299 -0
- autocoder/common/shell_commands/tests/test_execute_batch.py +588 -0
- autocoder/common/shell_commands/tests/test_indented_batch_commands.py +244 -0
- autocoder/common/shell_commands/tests/test_integration.py +664 -0
- autocoder/common/shell_commands/tests/test_monitoring.py +546 -0
- autocoder/common/shell_commands/tests/test_performance.py +632 -0
- autocoder/common/shell_commands/tests/test_process_cleanup.py +397 -0
- autocoder/common/shell_commands/tests/test_process_manager.py +606 -0
- autocoder/common/shell_commands/tests/test_timeout_config.py +343 -0
- autocoder/common/shell_commands/tests/test_timeout_manager.py +520 -0
- autocoder/common/shell_commands/timeout_config.py +315 -0
- autocoder/common/shell_commands/timeout_manager.py +352 -0
- autocoder/common/terminal_paste/__init__.py +14 -0
- autocoder/common/terminal_paste/demo.py +145 -0
- autocoder/common/terminal_paste/demo_paste_functionality.py +95 -0
- autocoder/common/terminal_paste/paste_handler.py +200 -0
- autocoder/common/terminal_paste/paste_manager.py +118 -0
- autocoder/common/terminal_paste/tests/__init__.py +1 -0
- autocoder/common/terminal_paste/tests/test_paste_handler.py +182 -0
- autocoder/common/terminal_paste/tests/test_paste_manager.py +126 -0
- autocoder/common/terminal_paste/utils.py +163 -0
- autocoder/common/test_autocoder_args.py +232 -0
- autocoder/common/test_env_manager.py +173 -0
- autocoder/common/test_env_manager_integration.py +159 -0
- autocoder/common/text_similarity/__init__.py +9 -0
- autocoder/common/text_similarity/demo.py +216 -0
- autocoder/common/text_similarity/examples.py +266 -0
- autocoder/common/text_similarity/test_text_similarity.py +306 -0
- autocoder/common/text_similarity/text_similarity.py +194 -0
- autocoder/common/text_similarity/utils.py +125 -0
- autocoder/common/todos/__init__.py +61 -0
- autocoder/common/todos/cache/__init__.py +16 -0
- autocoder/common/todos/cache/base_cache.py +89 -0
- autocoder/common/todos/cache/cache_manager.py +228 -0
- autocoder/common/todos/cache/memory_cache.py +225 -0
- autocoder/common/todos/config.py +155 -0
- autocoder/common/todos/exceptions.py +35 -0
- autocoder/common/todos/get_todo_manager.py +161 -0
- autocoder/common/todos/manager.py +537 -0
- autocoder/common/todos/models.py +239 -0
- autocoder/common/todos/storage/__init__.py +14 -0
- autocoder/common/todos/storage/base_storage.py +76 -0
- autocoder/common/todos/storage/file_storage.py +278 -0
- autocoder/common/tokens/counter.py +24 -2
- autocoder/common/tools_manager/__init__.py +17 -0
- autocoder/common/tools_manager/examples.py +162 -0
- autocoder/common/tools_manager/manager.py +385 -0
- autocoder/common/tools_manager/models.py +39 -0
- autocoder/common/tools_manager/test_tools_manager.py +303 -0
- autocoder/common/tools_manager/utils.py +191 -0
- autocoder/common/v2/agent/agentic_callbacks.py +270 -0
- autocoder/common/v2/agent/agentic_edit.py +2699 -1856
- autocoder/common/v2/agent/agentic_edit_change_manager.py +474 -0
- autocoder/common/v2/agent/agentic_edit_tools/__init__.py +35 -1
- autocoder/common/v2/agent/agentic_edit_tools/ac_mod_list_tool_resolver.py +279 -0
- autocoder/common/v2/agent/agentic_edit_tools/ac_mod_write_tool_resolver.py +10 -1
- autocoder/common/v2/agent/agentic_edit_tools/background_task_tool_resolver.py +1167 -0
- autocoder/common/v2/agent/agentic_edit_tools/base_tool_resolver.py +2 -2
- autocoder/common/v2/agent/agentic_edit_tools/conversation_message_ids_read_tool_resolver.py +214 -0
- autocoder/common/v2/agent/agentic_edit_tools/conversation_message_ids_write_tool_resolver.py +299 -0
- autocoder/common/v2/agent/agentic_edit_tools/count_tokens_tool_resolver.py +290 -0
- autocoder/common/v2/agent/agentic_edit_tools/execute_command_tool_resolver.py +564 -29
- autocoder/common/v2/agent/agentic_edit_tools/execute_workflow_tool_resolver.py +485 -0
- autocoder/common/v2/agent/agentic_edit_tools/extract_to_text_tool_resolver.py +225 -0
- autocoder/common/v2/agent/agentic_edit_tools/lint_report.py +79 -0
- autocoder/common/v2/agent/agentic_edit_tools/linter_config_models.py +343 -0
- autocoder/common/v2/agent/agentic_edit_tools/linter_enabled_tool_resolver.py +189 -0
- autocoder/common/v2/agent/agentic_edit_tools/list_files_tool_resolver.py +169 -101
- autocoder/common/v2/agent/agentic_edit_tools/load_extra_document_tool_resolver.py +349 -0
- autocoder/common/v2/agent/agentic_edit_tools/read_file_tool_resolver.py +243 -50
- autocoder/common/v2/agent/agentic_edit_tools/replace_in_file_tool_resolver.py +667 -147
- autocoder/common/v2/agent/agentic_edit_tools/run_named_subagents_tool_resolver.py +691 -0
- autocoder/common/v2/agent/agentic_edit_tools/search_files_tool_resolver.py +410 -86
- autocoder/common/v2/agent/agentic_edit_tools/session_interactive_tool_resolver.py +115 -0
- autocoder/common/v2/agent/agentic_edit_tools/session_start_tool_resolver.py +190 -0
- autocoder/common/v2/agent/agentic_edit_tools/session_stop_tool_resolver.py +76 -0
- autocoder/common/v2/agent/agentic_edit_tools/test_write_to_file_tool_resolver.py +207 -192
- autocoder/common/v2/agent/agentic_edit_tools/todo_read_tool_resolver.py +80 -63
- autocoder/common/v2/agent/agentic_edit_tools/todo_write_tool_resolver.py +237 -233
- autocoder/common/v2/agent/agentic_edit_tools/use_mcp_tool_resolver.py +2 -2
- autocoder/common/v2/agent/agentic_edit_tools/web_crawl_tool_resolver.py +557 -0
- autocoder/common/v2/agent/agentic_edit_tools/web_search_tool_resolver.py +600 -0
- autocoder/common/v2/agent/agentic_edit_tools/write_to_file_tool_resolver.py +56 -121
- autocoder/common/v2/agent/agentic_edit_types.py +343 -9
- autocoder/common/v2/agent/runner/__init__.py +3 -3
- autocoder/common/v2/agent/runner/base_runner.py +12 -26
- autocoder/common/v2/agent/runner/{event_runner.py → file_based_event_runner.py} +3 -2
- autocoder/common/v2/agent/runner/sdk_runner.py +150 -8
- autocoder/common/v2/agent/runner/terminal_runner.py +170 -57
- autocoder/common/v2/agent/runner/tool_display.py +557 -159
- autocoder/common/v2/agent/test_agentic_callbacks.py +265 -0
- autocoder/common/v2/agent/test_agentic_edit.py +194 -0
- autocoder/common/v2/agent/tool_caller/__init__.py +24 -0
- autocoder/common/v2/agent/tool_caller/default_tool_resolver_map.py +135 -0
- autocoder/common/v2/agent/tool_caller/integration_test.py +172 -0
- autocoder/common/v2/agent/tool_caller/plugins/__init__.py +14 -0
- autocoder/common/v2/agent/tool_caller/plugins/base_plugin.py +126 -0
- autocoder/common/v2/agent/tool_caller/plugins/examples/__init__.py +13 -0
- autocoder/common/v2/agent/tool_caller/plugins/examples/logging_plugin.py +164 -0
- autocoder/common/v2/agent/tool_caller/plugins/examples/security_filter_plugin.py +198 -0
- autocoder/common/v2/agent/tool_caller/plugins/plugin_interface.py +141 -0
- autocoder/common/v2/agent/tool_caller/test_tool_caller.py +278 -0
- autocoder/common/v2/agent/tool_caller/tool_call_plugin_manager.py +331 -0
- autocoder/common/v2/agent/tool_caller/tool_caller.py +337 -0
- autocoder/common/v2/agent/tool_caller/usage_example.py +193 -0
- autocoder/common/v2/code_agentic_editblock_manager.py +4 -4
- autocoder/common/v2/code_auto_generate.py +136 -78
- autocoder/common/v2/code_auto_generate_diff.py +135 -79
- autocoder/common/v2/code_auto_generate_editblock.py +174 -99
- autocoder/common/v2/code_auto_generate_strict_diff.py +151 -71
- autocoder/common/v2/code_auto_merge.py +1 -1
- autocoder/common/v2/code_auto_merge_editblock.py +13 -1
- autocoder/common/v2/code_diff_manager.py +3 -3
- autocoder/common/v2/code_editblock_manager.py +4 -14
- autocoder/common/v2/code_manager.py +1 -1
- autocoder/common/v2/code_strict_diff_manager.py +2 -2
- autocoder/common/wrap_llm_hint/__init__.py +10 -0
- autocoder/common/wrap_llm_hint/test_wrap_llm_hint.py +1067 -0
- autocoder/common/wrap_llm_hint/utils.py +432 -0
- autocoder/common/wrap_llm_hint/wrap_llm_hint.py +323 -0
- autocoder/completer/__init__.py +8 -0
- autocoder/completer/command_completer_v2.py +1051 -0
- autocoder/default_project/__init__.py +501 -0
- autocoder/dispacher/__init__.py +4 -12
- autocoder/dispacher/actions/action.py +165 -7
- autocoder/dispacher/actions/plugins/action_regex_project.py +2 -2
- autocoder/index/entry.py +116 -124
- autocoder/{agent → index/filter}/agentic_filter.py +322 -333
- autocoder/index/filter/normal_filter.py +5 -11
- autocoder/index/filter/quick_filter.py +1 -1
- autocoder/index/index.py +36 -9
- autocoder/index/tests/__init__.py +1 -0
- autocoder/index/tests/run_tests.py +195 -0
- autocoder/index/tests/test_entry.py +303 -0
- autocoder/index/tests/test_index_manager.py +314 -0
- autocoder/index/tests/test_module_integration.py +300 -0
- autocoder/index/tests/test_symbols_utils.py +183 -0
- autocoder/inner/__init__.py +4 -0
- autocoder/inner/agentic.py +932 -0
- autocoder/inner/async_command_handler.py +992 -0
- autocoder/inner/conversation_command_handlers.py +623 -0
- autocoder/inner/merge_command_handler.py +213 -0
- autocoder/inner/queue_command_handler.py +684 -0
- autocoder/models.py +95 -266
- autocoder/plugins/git_helper_plugin.py +31 -29
- autocoder/plugins/token_helper_plugin.py +65 -46
- autocoder/pyproject/__init__.py +32 -29
- autocoder/rag/agentic_rag.py +215 -75
- autocoder/rag/cache/simple_cache.py +1 -2
- autocoder/rag/loaders/image_loader.py +1 -1
- autocoder/rag/long_context_rag.py +42 -26
- autocoder/rag/qa_conversation_strategy.py +1 -1
- autocoder/rag/terminal/__init__.py +17 -0
- autocoder/rag/terminal/args.py +581 -0
- autocoder/rag/terminal/bootstrap.py +61 -0
- autocoder/rag/terminal/command_handlers.py +653 -0
- autocoder/rag/terminal/formatters/__init__.py +20 -0
- autocoder/rag/terminal/formatters/base.py +70 -0
- autocoder/rag/terminal/formatters/json_format.py +66 -0
- autocoder/rag/terminal/formatters/stream_json.py +95 -0
- autocoder/rag/terminal/formatters/text.py +28 -0
- autocoder/rag/terminal/init.py +120 -0
- autocoder/rag/terminal/utils.py +106 -0
- autocoder/rag/test_agentic_rag.py +389 -0
- autocoder/rag/test_doc_filter.py +3 -3
- autocoder/rag/test_long_context_rag.py +1 -1
- autocoder/rag/test_token_limiter.py +517 -10
- autocoder/rag/token_counter.py +3 -0
- autocoder/rag/token_limiter.py +19 -15
- autocoder/rag/tools/__init__.py +26 -2
- autocoder/rag/tools/bochaai_example.py +343 -0
- autocoder/rag/tools/bochaai_sdk.py +541 -0
- autocoder/rag/tools/metaso_example.py +268 -0
- autocoder/rag/tools/metaso_sdk.py +417 -0
- autocoder/rag/tools/recall_tool.py +28 -7
- autocoder/rag/tools/run_integration_tests.py +204 -0
- autocoder/rag/tools/test_all_providers.py +318 -0
- autocoder/rag/tools/test_bochaai_integration.py +482 -0
- autocoder/rag/tools/test_final_integration.py +215 -0
- autocoder/rag/tools/test_metaso_integration.py +424 -0
- autocoder/rag/tools/test_metaso_real.py +171 -0
- autocoder/rag/tools/test_web_crawl_tool.py +639 -0
- autocoder/rag/tools/test_web_search_tool.py +509 -0
- autocoder/rag/tools/todo_read_tool.py +202 -0
- autocoder/rag/tools/todo_write_tool.py +412 -0
- autocoder/rag/tools/web_crawl_tool.py +634 -0
- autocoder/rag/tools/web_search_tool.py +558 -0
- autocoder/rag/tools/web_tools_example.py +119 -0
- autocoder/rag/types.py +16 -0
- autocoder/rag/variable_holder.py +4 -2
- autocoder/rags.py +86 -79
- autocoder/regexproject/__init__.py +23 -21
- autocoder/sdk/__init__.py +46 -190
- autocoder/sdk/api.py +370 -0
- autocoder/sdk/async_runner/__init__.py +26 -0
- autocoder/sdk/async_runner/async_executor.py +650 -0
- autocoder/sdk/async_runner/async_handler.py +356 -0
- autocoder/sdk/async_runner/markdown_processor.py +595 -0
- autocoder/sdk/async_runner/task_metadata.py +284 -0
- autocoder/sdk/async_runner/worktree_manager.py +438 -0
- autocoder/sdk/cli/__init__.py +2 -5
- autocoder/sdk/cli/formatters.py +28 -204
- autocoder/sdk/cli/handlers.py +77 -44
- autocoder/sdk/cli/main.py +154 -171
- autocoder/sdk/cli/options.py +95 -22
- autocoder/sdk/constants.py +139 -51
- autocoder/sdk/core/auto_coder_core.py +484 -109
- autocoder/sdk/core/bridge.py +297 -115
- autocoder/sdk/exceptions.py +18 -12
- autocoder/sdk/formatters/__init__.py +19 -0
- autocoder/sdk/formatters/input.py +64 -0
- autocoder/sdk/formatters/output.py +247 -0
- autocoder/sdk/formatters/stream.py +54 -0
- autocoder/sdk/models/__init__.py +6 -5
- autocoder/sdk/models/options.py +55 -18
- autocoder/sdk/utils/formatters.py +27 -195
- autocoder/suffixproject/__init__.py +28 -25
- autocoder/terminal/__init__.py +14 -0
- autocoder/terminal/app.py +454 -0
- autocoder/terminal/args.py +32 -0
- autocoder/terminal/bootstrap.py +178 -0
- autocoder/terminal/command_processor.py +521 -0
- autocoder/terminal/command_registry.py +57 -0
- autocoder/terminal/help.py +97 -0
- autocoder/terminal/tasks/__init__.py +5 -0
- autocoder/terminal/tasks/background.py +77 -0
- autocoder/terminal/tasks/task_event.py +70 -0
- autocoder/terminal/ui/__init__.py +13 -0
- autocoder/terminal/ui/completer.py +268 -0
- autocoder/terminal/ui/keybindings.py +75 -0
- autocoder/terminal/ui/session.py +41 -0
- autocoder/terminal/ui/toolbar.py +64 -0
- autocoder/terminal/utils/__init__.py +13 -0
- autocoder/terminal/utils/errors.py +18 -0
- autocoder/terminal/utils/paths.py +19 -0
- autocoder/terminal/utils/shell.py +43 -0
- autocoder/terminal_v3/__init__.py +10 -0
- autocoder/terminal_v3/app.py +201 -0
- autocoder/terminal_v3/handlers/__init__.py +5 -0
- autocoder/terminal_v3/handlers/command_handler.py +131 -0
- autocoder/terminal_v3/models/__init__.py +6 -0
- autocoder/terminal_v3/models/conversation_buffer.py +214 -0
- autocoder/terminal_v3/models/message.py +50 -0
- autocoder/terminal_v3/models/tool_display.py +247 -0
- autocoder/terminal_v3/ui/__init__.py +7 -0
- autocoder/terminal_v3/ui/keybindings.py +56 -0
- autocoder/terminal_v3/ui/layout.py +141 -0
- autocoder/terminal_v3/ui/styles.py +43 -0
- autocoder/tsproject/__init__.py +23 -23
- autocoder/utils/auto_coder_utils/chat_stream_out.py +1 -1
- autocoder/utils/llms.py +88 -80
- autocoder/utils/math_utils.py +101 -0
- autocoder/utils/model_provider_selector.py +16 -4
- autocoder/utils/operate_config_api.py +33 -5
- autocoder/utils/thread_utils.py +2 -2
- autocoder/version.py +4 -2
- autocoder/workflow_agents/__init__.py +84 -0
- autocoder/workflow_agents/agent.py +143 -0
- autocoder/workflow_agents/exceptions.py +573 -0
- autocoder/workflow_agents/executor.py +489 -0
- autocoder/workflow_agents/loader.py +737 -0
- autocoder/workflow_agents/runner.py +267 -0
- autocoder/workflow_agents/types.py +172 -0
- autocoder/workflow_agents/utils.py +434 -0
- autocoder/workflow_agents/workflow_manager.py +211 -0
- auto_coder-1.0.0.dist-info/METADATA +0 -396
- auto_coder-1.0.0.dist-info/RECORD +0 -442
- auto_coder-1.0.0.dist-info/licenses/LICENSE +0 -201
- autocoder/auto_coder_server.py +0 -672
- autocoder/benchmark.py +0 -138
- autocoder/common/ac_style_command_parser/example.py +0 -7
- autocoder/common/cleaner.py +0 -31
- autocoder/common/command_completer_v2.py +0 -615
- autocoder/common/context_pruner.py +0 -477
- autocoder/common/conversation_pruner.py +0 -132
- autocoder/common/directory_cache/__init__.py +0 -1
- autocoder/common/directory_cache/cache.py +0 -192
- autocoder/common/directory_cache/test_cache.py +0 -190
- autocoder/common/file_checkpoint/examples.py +0 -217
- autocoder/common/llm_friendly_package_example.py +0 -138
- autocoder/common/llm_friendly_package_test.py +0 -63
- autocoder/common/pull_requests/test_module.py +0 -1
- autocoder/common/rulefiles/autocoderrules_utils.py +0 -484
- autocoder/common/text.py +0 -30
- autocoder/common/v2/agent/agentic_edit_tools/list_package_info_tool_resolver.py +0 -42
- autocoder/common/v2/agent/agentic_edit_tools/test_execute_command_tool_resolver.py +0 -70
- autocoder/common/v2/agent/agentic_edit_tools/test_search_files_tool_resolver.py +0 -163
- autocoder/common/v2/agent/agentic_tool_display.py +0 -183
- autocoder/plugins/dynamic_completion_example.py +0 -148
- autocoder/plugins/sample_plugin.py +0 -160
- autocoder/sdk/cli/__main__.py +0 -26
- autocoder/sdk/cli/completion_wrapper.py +0 -38
- autocoder/sdk/cli/install_completion.py +0 -301
- autocoder/sdk/models/messages.py +0 -209
- autocoder/sdk/session/__init__.py +0 -32
- autocoder/sdk/session/session.py +0 -106
- autocoder/sdk/session/session_manager.py +0 -56
- {auto_coder-1.0.0.dist-info → auto_coder-2.0.0.dist-info}/top_level.txt +0 -0
- /autocoder/{sdk/example.py → common/agent_query_queue/__init__.py} +0 -0
|
@@ -0,0 +1,1014 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Interactive session manager for handling multiple interactive command sessions.
|
|
3
|
+
|
|
4
|
+
This module provides a thread-safe singleton manager for handling multiple
|
|
5
|
+
interactive command sessions with support for both InteractiveProcess and
|
|
6
|
+
InteractivePexpectProcess backends, automatic cleanup and resource management.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import threading
|
|
10
|
+
import time
|
|
11
|
+
import uuid
|
|
12
|
+
import atexit
|
|
13
|
+
import re
|
|
14
|
+
import platform
|
|
15
|
+
from typing import Dict, Optional, Any, List, Union, Type
|
|
16
|
+
from loguru import logger
|
|
17
|
+
|
|
18
|
+
from .interactive_executor import InteractiveCommandExecutor, InteractiveSession
|
|
19
|
+
from .interactive_process import InteractiveProcess
|
|
20
|
+
from .interactive_pexpect_process import InteractivePexpectProcess, PEXPECT_AVAILABLE
|
|
21
|
+
from .exceptions import CommandExecutionError
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def clean_terminal_output(text: str) -> str:
|
|
25
|
+
"""
|
|
26
|
+
Clean terminal control characters and ANSI escape sequences from output.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
text: Raw terminal output
|
|
30
|
+
|
|
31
|
+
Returns:
|
|
32
|
+
Cleaned text with control characters removed
|
|
33
|
+
"""
|
|
34
|
+
if not text:
|
|
35
|
+
return text
|
|
36
|
+
# return text
|
|
37
|
+
|
|
38
|
+
# Remove ANSI escape sequences
|
|
39
|
+
ansi_escape = re.compile(r'\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])')
|
|
40
|
+
text = ansi_escape.sub('', text)
|
|
41
|
+
|
|
42
|
+
# Normalize Windows CRLF to LF first
|
|
43
|
+
text = text.replace('\r\n', '\n').replace('\r', '\n')
|
|
44
|
+
|
|
45
|
+
# Handle standalone carriage returns (\r) that are used to return cursor to
|
|
46
|
+
# the beginning of the line (common in interactive shells). We simulate the
|
|
47
|
+
# overwrite by keeping only the content AFTER the last \r in each logical
|
|
48
|
+
# line.
|
|
49
|
+
if '\r' in text:
|
|
50
|
+
processed_lines = []
|
|
51
|
+
for raw_line in text.split('\n'):
|
|
52
|
+
# Repeatedly process carriage returns within the line
|
|
53
|
+
while '\r' in raw_line:
|
|
54
|
+
raw_line = raw_line.split('\r')[-1]
|
|
55
|
+
processed_lines.append(raw_line)
|
|
56
|
+
text = '\n'.join(processed_lines)
|
|
57
|
+
|
|
58
|
+
# Remove other control characters but keep newlines and tabs
|
|
59
|
+
control_chars = re.compile(r'[\x00-\x08\x0B-\x0C\x0E-\x1F\x7F]')
|
|
60
|
+
text = control_chars.sub('', text)
|
|
61
|
+
|
|
62
|
+
# Clean up multiple consecutive newlines
|
|
63
|
+
text = re.sub(r'\n\s*\n\s*\n+', '\n\n', text)
|
|
64
|
+
|
|
65
|
+
# Strip leading/trailing whitespace
|
|
66
|
+
text = text.strip()
|
|
67
|
+
|
|
68
|
+
return text
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class ProcessType:
|
|
72
|
+
"""Constants for process types."""
|
|
73
|
+
STANDARD = "standard"
|
|
74
|
+
PEXPECT = "pexpect"
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ToolResult:
|
|
78
|
+
"""Result object for tool operations."""
|
|
79
|
+
|
|
80
|
+
def __init__(self, success: bool, message: str, content: Optional[Union[Dict[str, Any], str]] = None):
|
|
81
|
+
self.success = success
|
|
82
|
+
self.message = message
|
|
83
|
+
self.content = content or {}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class EnhancedSessionHandle:
|
|
87
|
+
"""Enhanced handle for managing an interactive session with process type support."""
|
|
88
|
+
|
|
89
|
+
def __init__(
|
|
90
|
+
self,
|
|
91
|
+
session: InteractiveSession,
|
|
92
|
+
command: str,
|
|
93
|
+
process_type: str,
|
|
94
|
+
cwd: Optional[str] = None
|
|
95
|
+
):
|
|
96
|
+
self.session = session
|
|
97
|
+
self.command = command
|
|
98
|
+
self.process_type = process_type
|
|
99
|
+
self.cwd = cwd
|
|
100
|
+
self.created_at = time.time()
|
|
101
|
+
self.last_activity = time.time()
|
|
102
|
+
|
|
103
|
+
def update_activity(self):
|
|
104
|
+
"""Update the last activity timestamp."""
|
|
105
|
+
self.last_activity = time.time()
|
|
106
|
+
|
|
107
|
+
def get_stats(self) -> Dict[str, Any]:
|
|
108
|
+
"""Get session statistics."""
|
|
109
|
+
return {
|
|
110
|
+
'session_id': self.session.session_id,
|
|
111
|
+
'command': self.command,
|
|
112
|
+
'process_type': self.process_type,
|
|
113
|
+
'cwd': self.cwd,
|
|
114
|
+
'pid': self.session.process.pid if self.session.process else None,
|
|
115
|
+
'created_at': self.created_at,
|
|
116
|
+
'last_activity': self.last_activity,
|
|
117
|
+
'duration': time.time() - self.created_at,
|
|
118
|
+
'idle_time': time.time() - self.last_activity,
|
|
119
|
+
'is_alive': self.session.process.is_alive() if self.session.process else False
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class InteractiveSessionManager:
|
|
124
|
+
"""Thread-safe singleton manager for interactive command sessions with process type switching."""
|
|
125
|
+
|
|
126
|
+
_instance = None
|
|
127
|
+
_lock = threading.Lock()
|
|
128
|
+
|
|
129
|
+
def __new__(cls):
|
|
130
|
+
if cls._instance is None:
|
|
131
|
+
with cls._lock:
|
|
132
|
+
if cls._instance is None:
|
|
133
|
+
cls._instance = super().__new__(cls)
|
|
134
|
+
cls._instance._initialized = False
|
|
135
|
+
return cls._instance
|
|
136
|
+
|
|
137
|
+
def __init__(self):
|
|
138
|
+
if self._initialized:
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
self._initialized = True
|
|
142
|
+
self.sessions: Dict[str, EnhancedSessionHandle] = {}
|
|
143
|
+
self.executor = InteractiveCommandExecutor(verbose=False)
|
|
144
|
+
self._cleanup_lock = threading.Lock()
|
|
145
|
+
self._cleanup_thread = None
|
|
146
|
+
self._shutdown = False
|
|
147
|
+
|
|
148
|
+
# Register cleanup on exit
|
|
149
|
+
atexit.register(self.cleanup_all)
|
|
150
|
+
|
|
151
|
+
# Start cleanup thread
|
|
152
|
+
self._start_cleanup_thread()
|
|
153
|
+
|
|
154
|
+
logger.info("InteractiveSessionManager initialized with process type switching support")
|
|
155
|
+
|
|
156
|
+
def _start_cleanup_thread(self):
|
|
157
|
+
"""Start the background cleanup thread."""
|
|
158
|
+
def cleanup_worker():
|
|
159
|
+
while not self._shutdown:
|
|
160
|
+
try:
|
|
161
|
+
self._cleanup_idle_sessions()
|
|
162
|
+
time.sleep(60) # Check every minute
|
|
163
|
+
except Exception as e:
|
|
164
|
+
logger.error(f"Error in cleanup thread: {e}")
|
|
165
|
+
time.sleep(60)
|
|
166
|
+
|
|
167
|
+
self._cleanup_thread = threading.Thread(target=cleanup_worker, daemon=True)
|
|
168
|
+
self._cleanup_thread.start()
|
|
169
|
+
|
|
170
|
+
def _cleanup_idle_sessions(self):
|
|
171
|
+
"""Clean up idle sessions (30 minutes of inactivity)."""
|
|
172
|
+
current_time = time.time()
|
|
173
|
+
idle_timeout = 30 * 60 # 30 minutes
|
|
174
|
+
|
|
175
|
+
with self._cleanup_lock:
|
|
176
|
+
to_remove = []
|
|
177
|
+
for session_id, handle in self.sessions.items():
|
|
178
|
+
if current_time - handle.last_activity > idle_timeout:
|
|
179
|
+
logger.info(f"Cleaning up idle session {session_id} (type: {handle.process_type})")
|
|
180
|
+
try:
|
|
181
|
+
handle.session.terminate()
|
|
182
|
+
except Exception as e:
|
|
183
|
+
logger.error(f"Error terminating idle session {session_id}: {e}")
|
|
184
|
+
to_remove.append(session_id)
|
|
185
|
+
|
|
186
|
+
for session_id in to_remove:
|
|
187
|
+
self.sessions.pop(session_id, None)
|
|
188
|
+
|
|
189
|
+
def _determine_process_type(
|
|
190
|
+
self,
|
|
191
|
+
command: str,
|
|
192
|
+
use_pexpect: Optional[bool] = None
|
|
193
|
+
) -> str:
|
|
194
|
+
"""
|
|
195
|
+
Determine the best process type for a command.
|
|
196
|
+
|
|
197
|
+
Args:
|
|
198
|
+
command: Command to execute
|
|
199
|
+
use_pexpect: Explicit process type selection
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Process type string
|
|
203
|
+
"""
|
|
204
|
+
# Explicit selection
|
|
205
|
+
if use_pexpect is not None:
|
|
206
|
+
if use_pexpect:
|
|
207
|
+
if PEXPECT_AVAILABLE:
|
|
208
|
+
return ProcessType.PEXPECT
|
|
209
|
+
else:
|
|
210
|
+
logger.warning("pexpect requested but not available, falling back to standard process")
|
|
211
|
+
return ProcessType.STANDARD
|
|
212
|
+
else:
|
|
213
|
+
return ProcessType.STANDARD
|
|
214
|
+
|
|
215
|
+
# Automatic selection based on command and platform
|
|
216
|
+
if not PEXPECT_AVAILABLE:
|
|
217
|
+
return ProcessType.STANDARD
|
|
218
|
+
|
|
219
|
+
# Commands that benefit from pexpect
|
|
220
|
+
interactive_commands = [
|
|
221
|
+
'python', 'python3', 'node', 'repl', 'irb', 'ghci',
|
|
222
|
+
'mysql', 'psql', 'sqlite3', 'redis-cli', 'mongo',
|
|
223
|
+
'ssh', 'telnet', 'ftp', 'sftp'
|
|
224
|
+
]
|
|
225
|
+
|
|
226
|
+
cmd_lower = command.lower()
|
|
227
|
+
for interactive_cmd in interactive_commands:
|
|
228
|
+
if cmd_lower.startswith(interactive_cmd):
|
|
229
|
+
logger.debug(f"Auto-selecting pexpect for interactive command: {command}")
|
|
230
|
+
return ProcessType.PEXPECT
|
|
231
|
+
|
|
232
|
+
# Default to standard process
|
|
233
|
+
return ProcessType.STANDARD
|
|
234
|
+
|
|
235
|
+
def _create_process(
|
|
236
|
+
self,
|
|
237
|
+
command: str,
|
|
238
|
+
process_type: str,
|
|
239
|
+
cwd: Optional[str] = None,
|
|
240
|
+
env: Optional[Dict[str, str]] = None,
|
|
241
|
+
timeout: Optional[int] = None,
|
|
242
|
+
**kwargs
|
|
243
|
+
) -> Union[InteractiveProcess, InteractivePexpectProcess]:
|
|
244
|
+
"""
|
|
245
|
+
Create a process of the specified type.
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
command: Command to execute
|
|
249
|
+
process_type: Type of process to create
|
|
250
|
+
cwd: Working directory
|
|
251
|
+
env: Environment variables
|
|
252
|
+
timeout: Process timeout
|
|
253
|
+
**kwargs: Additional process arguments
|
|
254
|
+
|
|
255
|
+
Returns:
|
|
256
|
+
Process instance
|
|
257
|
+
|
|
258
|
+
Raises:
|
|
259
|
+
CommandExecutionError: If process creation fails
|
|
260
|
+
"""
|
|
261
|
+
try:
|
|
262
|
+
if process_type == ProcessType.PEXPECT:
|
|
263
|
+
if not PEXPECT_AVAILABLE:
|
|
264
|
+
raise CommandExecutionError("pexpect not available on this platform")
|
|
265
|
+
|
|
266
|
+
process = InteractivePexpectProcess(
|
|
267
|
+
command=command,
|
|
268
|
+
cwd=cwd,
|
|
269
|
+
env=env,
|
|
270
|
+
timeout=timeout,
|
|
271
|
+
**kwargs
|
|
272
|
+
)
|
|
273
|
+
else:
|
|
274
|
+
process = InteractiveProcess(
|
|
275
|
+
command=command,
|
|
276
|
+
cwd=cwd,
|
|
277
|
+
env=env,
|
|
278
|
+
**kwargs
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
process.start()
|
|
282
|
+
return process
|
|
283
|
+
|
|
284
|
+
except Exception as e:
|
|
285
|
+
# Fallback to standard process if pexpect fails
|
|
286
|
+
if process_type == ProcessType.PEXPECT:
|
|
287
|
+
logger.warning(f"pexpect process creation failed, falling back to standard: {e}")
|
|
288
|
+
return self._create_process(
|
|
289
|
+
command, ProcessType.STANDARD, cwd, env, timeout, **kwargs
|
|
290
|
+
)
|
|
291
|
+
else:
|
|
292
|
+
raise CommandExecutionError(f"Failed to create process: {e}")
|
|
293
|
+
|
|
294
|
+
def create_session(
|
|
295
|
+
self,
|
|
296
|
+
command: str,
|
|
297
|
+
cwd: Optional[str] = None,
|
|
298
|
+
env: Optional[Dict[str, str]] = None,
|
|
299
|
+
timeout: Optional[int] = None,
|
|
300
|
+
use_pexpect: Optional[bool] = None,
|
|
301
|
+
session_id: Optional[str] = None,
|
|
302
|
+
**kwargs
|
|
303
|
+
) -> ToolResult:
|
|
304
|
+
"""
|
|
305
|
+
Create a new interactive session with automatic or manual process type selection.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
command: Command to execute
|
|
309
|
+
cwd: Working directory
|
|
310
|
+
env: Environment variables
|
|
311
|
+
timeout: Session timeout in seconds
|
|
312
|
+
use_pexpect: Explicit process type selection (None=auto, True=pexpect, False=standard)
|
|
313
|
+
session_id: Custom session ID
|
|
314
|
+
**kwargs: Additional process arguments
|
|
315
|
+
|
|
316
|
+
Returns:
|
|
317
|
+
ToolResult with session information
|
|
318
|
+
"""
|
|
319
|
+
try:
|
|
320
|
+
if session_id is None:
|
|
321
|
+
session_id = str(uuid.uuid4())
|
|
322
|
+
|
|
323
|
+
# Determine process type
|
|
324
|
+
process_type = self._determine_process_type(command, use_pexpect)
|
|
325
|
+
|
|
326
|
+
# Create process
|
|
327
|
+
process = self._create_process(
|
|
328
|
+
command=command,
|
|
329
|
+
process_type=process_type,
|
|
330
|
+
cwd=cwd,
|
|
331
|
+
env=env,
|
|
332
|
+
timeout=timeout,
|
|
333
|
+
**kwargs
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
# Create session wrapper
|
|
337
|
+
session = InteractiveSession(
|
|
338
|
+
process=process,
|
|
339
|
+
session_id=session_id,
|
|
340
|
+
timeout=timeout
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
# Create enhanced handle
|
|
344
|
+
handle = EnhancedSessionHandle(session, command, process_type, cwd)
|
|
345
|
+
|
|
346
|
+
# Store session
|
|
347
|
+
with self._cleanup_lock:
|
|
348
|
+
self.sessions[session_id] = handle
|
|
349
|
+
|
|
350
|
+
logger.info(f"Created interactive session {session_id} using {process_type} process for command: {command}")
|
|
351
|
+
|
|
352
|
+
return ToolResult(
|
|
353
|
+
success=True,
|
|
354
|
+
message=f"Interactive session started successfully using {process_type} process",
|
|
355
|
+
content={
|
|
356
|
+
'session_id': session_id,
|
|
357
|
+
'pid': process.pid if process else None,
|
|
358
|
+
'command': command,
|
|
359
|
+
'process_type': process_type,
|
|
360
|
+
'cwd': cwd,
|
|
361
|
+
'created_at': handle.created_at
|
|
362
|
+
}
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
except Exception as e:
|
|
366
|
+
logger.error(f"Failed to create interactive session: {e}")
|
|
367
|
+
return ToolResult(
|
|
368
|
+
success=False,
|
|
369
|
+
message=f"Failed to create interactive session: {str(e)}"
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
def send_input(
|
|
373
|
+
self,
|
|
374
|
+
session_id: str,
|
|
375
|
+
input_text: str,
|
|
376
|
+
read_timeout: Optional[int] = None,
|
|
377
|
+
max_bytes: Optional[int] = None,
|
|
378
|
+
expect_prompt: Optional[bool] = False,
|
|
379
|
+
prompt_regex: Optional[str] = r'>>> ?$'
|
|
380
|
+
) -> ToolResult:
|
|
381
|
+
"""
|
|
382
|
+
Send input to a session and read output with process-type-aware handling.
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
session_id: Session ID
|
|
386
|
+
input_text: Text to send to stdin
|
|
387
|
+
read_timeout: Timeout for reading output
|
|
388
|
+
max_bytes: Maximum bytes to read
|
|
389
|
+
expect_prompt: Whether to wait for prompt before returning
|
|
390
|
+
prompt_regex: Regular expression to match prompt
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
ToolResult with output
|
|
394
|
+
"""
|
|
395
|
+
with self._cleanup_lock:
|
|
396
|
+
handle = self.sessions.get(session_id)
|
|
397
|
+
if not handle:
|
|
398
|
+
return ToolResult(
|
|
399
|
+
success=False,
|
|
400
|
+
message=f"Session {session_id} not found"
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
# Update activity
|
|
404
|
+
handle.update_activity()
|
|
405
|
+
|
|
406
|
+
try:
|
|
407
|
+
# Check if session is still alive
|
|
408
|
+
if not handle.session.process.is_alive():
|
|
409
|
+
return ToolResult(
|
|
410
|
+
success=False,
|
|
411
|
+
message=f"Session {session_id} is no longer alive"
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
# Send input based on process type
|
|
415
|
+
if handle.process_type == ProcessType.PEXPECT:
|
|
416
|
+
# Use pexpect-specific methods
|
|
417
|
+
if hasattr(handle.session.process, 'sendline'):
|
|
418
|
+
# Remove trailing newline if present since sendline adds it
|
|
419
|
+
clean_input = input_text.rstrip('\n\r')
|
|
420
|
+
handle.session.process.sendline(clean_input)
|
|
421
|
+
else:
|
|
422
|
+
handle.session.process.write(input_text)
|
|
423
|
+
else:
|
|
424
|
+
# Use standard process write
|
|
425
|
+
handle.session.process.write(input_text)
|
|
426
|
+
|
|
427
|
+
# Read output based on expect_prompt setting and process type
|
|
428
|
+
if expect_prompt and handle.process_type == ProcessType.PEXPECT:
|
|
429
|
+
# Use pexpect expect functionality
|
|
430
|
+
raw_output = self._read_until_prompt_pexpect(
|
|
431
|
+
handle.session.process,
|
|
432
|
+
prompt_regex or r'>>> ?$',
|
|
433
|
+
read_timeout or 2
|
|
434
|
+
)
|
|
435
|
+
elif expect_prompt:
|
|
436
|
+
# Use standard process with prompt detection
|
|
437
|
+
raw_output = self._read_until_prompt(
|
|
438
|
+
handle.session.process,
|
|
439
|
+
prompt_regex or r'>>> ?$',
|
|
440
|
+
read_timeout or 2
|
|
441
|
+
)
|
|
442
|
+
else:
|
|
443
|
+
# Standard output reading
|
|
444
|
+
raw_output = handle.session.process.read_output(
|
|
445
|
+
timeout=read_timeout or 2
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
# Clean the output to remove terminal control characters
|
|
449
|
+
cleaned_output = clean_terminal_output(raw_output or '')
|
|
450
|
+
|
|
451
|
+
logger.debug(f"Session {session_id} ({handle.process_type}): sent input, got {len(raw_output or '')} bytes raw output, {len(cleaned_output)} chars cleaned, expect_prompt={expect_prompt}")
|
|
452
|
+
|
|
453
|
+
return ToolResult(
|
|
454
|
+
success=True,
|
|
455
|
+
message="Input sent and output received",
|
|
456
|
+
content={
|
|
457
|
+
'session_id': session_id,
|
|
458
|
+
'output': cleaned_output,
|
|
459
|
+
'raw_output': raw_output or '',
|
|
460
|
+
'input_sent': input_text,
|
|
461
|
+
'bytes_read': len(raw_output or ''),
|
|
462
|
+
'cleaned_length': len(cleaned_output),
|
|
463
|
+
'process_type': handle.process_type
|
|
464
|
+
}
|
|
465
|
+
)
|
|
466
|
+
|
|
467
|
+
except Exception as e:
|
|
468
|
+
logger.error(f"Error interacting with session {session_id}: {e}")
|
|
469
|
+
return ToolResult(
|
|
470
|
+
success=False,
|
|
471
|
+
message=f"Error interacting with session: {str(e)}"
|
|
472
|
+
)
|
|
473
|
+
|
|
474
|
+
def read_output(
|
|
475
|
+
self,
|
|
476
|
+
session_id: str,
|
|
477
|
+
read_timeout: Optional[int] = None,
|
|
478
|
+
max_bytes: Optional[int] = None,
|
|
479
|
+
expect_prompt: Optional[bool] = False,
|
|
480
|
+
prompt_regex: Optional[str] = r'>>> ?$'
|
|
481
|
+
) -> ToolResult:
|
|
482
|
+
"""
|
|
483
|
+
Read output from a session without sending any input.
|
|
484
|
+
|
|
485
|
+
Args:
|
|
486
|
+
session_id: Session ID
|
|
487
|
+
read_timeout: Timeout for reading output
|
|
488
|
+
max_bytes: Maximum bytes to read
|
|
489
|
+
expect_prompt: Whether to wait for prompt before returning
|
|
490
|
+
prompt_regex: Regular expression to match prompt
|
|
491
|
+
|
|
492
|
+
Returns:
|
|
493
|
+
ToolResult with output
|
|
494
|
+
"""
|
|
495
|
+
with self._cleanup_lock:
|
|
496
|
+
handle = self.sessions.get(session_id)
|
|
497
|
+
if not handle:
|
|
498
|
+
return ToolResult(
|
|
499
|
+
success=False,
|
|
500
|
+
message=f"Session {session_id} not found"
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
# Update activity
|
|
504
|
+
handle.update_activity()
|
|
505
|
+
|
|
506
|
+
try:
|
|
507
|
+
# Check if session is still alive
|
|
508
|
+
if not handle.session.process.is_alive():
|
|
509
|
+
return ToolResult(
|
|
510
|
+
success=False,
|
|
511
|
+
message=f"Session {session_id} is no longer alive"
|
|
512
|
+
)
|
|
513
|
+
|
|
514
|
+
# Read output based on process type and expect_prompt setting
|
|
515
|
+
if expect_prompt and handle.process_type == ProcessType.PEXPECT:
|
|
516
|
+
raw_output = self._read_until_prompt_pexpect(
|
|
517
|
+
handle.session.process,
|
|
518
|
+
prompt_regex or r'>>> ?$',
|
|
519
|
+
read_timeout or 2
|
|
520
|
+
)
|
|
521
|
+
elif expect_prompt:
|
|
522
|
+
raw_output = self._read_until_prompt(
|
|
523
|
+
handle.session.process,
|
|
524
|
+
prompt_regex or r'>>> ?$',
|
|
525
|
+
read_timeout or 2
|
|
526
|
+
)
|
|
527
|
+
else:
|
|
528
|
+
raw_output = handle.session.process.read_output(
|
|
529
|
+
timeout=read_timeout or 2
|
|
530
|
+
)
|
|
531
|
+
|
|
532
|
+
# Clean the output to remove terminal control characters
|
|
533
|
+
cleaned_output = clean_terminal_output(raw_output or '')
|
|
534
|
+
|
|
535
|
+
logger.debug(f"Session {session_id} ({handle.process_type}): read output only, got {len(raw_output or '')} bytes raw output, {len(cleaned_output)} chars cleaned, expect_prompt={expect_prompt}")
|
|
536
|
+
|
|
537
|
+
return ToolResult(
|
|
538
|
+
success=True,
|
|
539
|
+
message="Output read successfully",
|
|
540
|
+
content={
|
|
541
|
+
'session_id': session_id,
|
|
542
|
+
'output': cleaned_output,
|
|
543
|
+
'raw_output': raw_output or '',
|
|
544
|
+
'input_sent': None,
|
|
545
|
+
'bytes_read': len(raw_output or ''),
|
|
546
|
+
'cleaned_length': len(cleaned_output),
|
|
547
|
+
'process_type': handle.process_type
|
|
548
|
+
}
|
|
549
|
+
)
|
|
550
|
+
|
|
551
|
+
except Exception as e:
|
|
552
|
+
logger.error(f"Error reading output from session {session_id}: {e}")
|
|
553
|
+
return ToolResult(
|
|
554
|
+
success=False,
|
|
555
|
+
message=f"Error reading output from session: {str(e)}"
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
def read_output_progressive(
|
|
559
|
+
self,
|
|
560
|
+
session_id: str,
|
|
561
|
+
read_timeout: Optional[int] = None,
|
|
562
|
+
max_bytes: Optional[int] = None,
|
|
563
|
+
expect_prompt: Optional[bool] = False,
|
|
564
|
+
prompt_regex: Optional[str] = r'>>> ?$'
|
|
565
|
+
) -> ToolResult:
|
|
566
|
+
"""
|
|
567
|
+
Progressively read output from a session with intelligent timeout handling.
|
|
568
|
+
|
|
569
|
+
This method waits for initial output, then continues reading as long as content
|
|
570
|
+
is available, stopping only after 5 seconds of no new content.
|
|
571
|
+
|
|
572
|
+
Args:
|
|
573
|
+
session_id: Session ID
|
|
574
|
+
read_timeout: Initial timeout for reading output (default: 3 seconds)
|
|
575
|
+
max_bytes: Maximum bytes to read in total
|
|
576
|
+
expect_prompt: Whether to wait for prompt before returning
|
|
577
|
+
prompt_regex: Regular expression to match prompt
|
|
578
|
+
|
|
579
|
+
Returns:
|
|
580
|
+
ToolResult with accumulated output
|
|
581
|
+
"""
|
|
582
|
+
with self._cleanup_lock:
|
|
583
|
+
handle = self.sessions.get(session_id)
|
|
584
|
+
if not handle:
|
|
585
|
+
return ToolResult(
|
|
586
|
+
success=False,
|
|
587
|
+
message=f"Session {session_id} not found"
|
|
588
|
+
)
|
|
589
|
+
|
|
590
|
+
# Update activity
|
|
591
|
+
handle.update_activity()
|
|
592
|
+
|
|
593
|
+
try:
|
|
594
|
+
# Check if session is still alive
|
|
595
|
+
if not handle.session.process.is_alive():
|
|
596
|
+
return ToolResult(
|
|
597
|
+
success=False,
|
|
598
|
+
message=f"Session {session_id} is no longer alive"
|
|
599
|
+
)
|
|
600
|
+
|
|
601
|
+
# Progressive reading logic
|
|
602
|
+
initial_timeout = read_timeout or 3
|
|
603
|
+
accumulated_output = ""
|
|
604
|
+
total_bytes_read = 0
|
|
605
|
+
read_cycles = 0
|
|
606
|
+
no_content_duration = 0
|
|
607
|
+
last_read_time = time.time()
|
|
608
|
+
|
|
609
|
+
logger.debug(f"Session {session_id} ({handle.process_type}): Starting progressive read with initial timeout {initial_timeout}s")
|
|
610
|
+
|
|
611
|
+
while True:
|
|
612
|
+
read_cycles += 1
|
|
613
|
+
cycle_start_time = time.time()
|
|
614
|
+
|
|
615
|
+
# Read with current timeout
|
|
616
|
+
if expect_prompt and accumulated_output:
|
|
617
|
+
# If we already have some output and expect prompt, check if we found it
|
|
618
|
+
lines = accumulated_output.split('\n')
|
|
619
|
+
if lines and re.search(prompt_regex or r'>>> ?$', lines[-1]):
|
|
620
|
+
logger.debug(f"Session {session_id}: Found prompt pattern, stopping progressive read")
|
|
621
|
+
break
|
|
622
|
+
|
|
623
|
+
# Read a chunk
|
|
624
|
+
chunk = handle.session.process.read_output(timeout=initial_timeout)
|
|
625
|
+
|
|
626
|
+
if chunk and chunk.strip():
|
|
627
|
+
# Got content, reset no-content counter
|
|
628
|
+
accumulated_output += chunk
|
|
629
|
+
total_bytes_read += len(chunk)
|
|
630
|
+
no_content_duration = 0
|
|
631
|
+
last_read_time = time.time()
|
|
632
|
+
|
|
633
|
+
logger.debug(f"Session {session_id}: Read cycle {read_cycles}, got {len(chunk)} bytes, total: {total_bytes_read} bytes")
|
|
634
|
+
|
|
635
|
+
# Check max_bytes limit
|
|
636
|
+
if max_bytes and total_bytes_read >= max_bytes:
|
|
637
|
+
logger.debug(f"Session {session_id}: Reached max_bytes limit ({max_bytes}), stopping")
|
|
638
|
+
break
|
|
639
|
+
|
|
640
|
+
else:
|
|
641
|
+
# No content in this cycle
|
|
642
|
+
current_time = time.time()
|
|
643
|
+
no_content_duration += current_time - cycle_start_time
|
|
644
|
+
|
|
645
|
+
logger.debug(f"Session {session_id}: Read cycle {read_cycles}, no content for {no_content_duration:.1f}s")
|
|
646
|
+
|
|
647
|
+
# If we have some content and no new content for 5 seconds, stop
|
|
648
|
+
if accumulated_output and no_content_duration >= 5.0:
|
|
649
|
+
logger.debug(f"Session {session_id}: No new content for 5 seconds, stopping progressive read")
|
|
650
|
+
break
|
|
651
|
+
|
|
652
|
+
# If we have no content at all and waited initial timeout, stop
|
|
653
|
+
if not accumulated_output and no_content_duration >= initial_timeout:
|
|
654
|
+
logger.debug(f"Session {session_id}: No initial content after {initial_timeout}s, stopping")
|
|
655
|
+
break
|
|
656
|
+
|
|
657
|
+
# Brief pause between cycles to avoid excessive CPU usage
|
|
658
|
+
time.sleep(0.1)
|
|
659
|
+
|
|
660
|
+
# Clean the accumulated output
|
|
661
|
+
cleaned_output = clean_terminal_output(accumulated_output)
|
|
662
|
+
|
|
663
|
+
logger.debug(f"Session {session_id} ({handle.process_type}): Progressive read completed after {read_cycles} cycles, "
|
|
664
|
+
f"got {total_bytes_read} bytes raw output, {len(cleaned_output)} chars cleaned")
|
|
665
|
+
|
|
666
|
+
return ToolResult(
|
|
667
|
+
success=True,
|
|
668
|
+
message=f"Progressive output read completed ({read_cycles} cycles)",
|
|
669
|
+
content={
|
|
670
|
+
'session_id': session_id,
|
|
671
|
+
'output': cleaned_output,
|
|
672
|
+
'raw_output': accumulated_output,
|
|
673
|
+
'input_sent': None,
|
|
674
|
+
'bytes_read': total_bytes_read,
|
|
675
|
+
'cleaned_length': len(cleaned_output),
|
|
676
|
+
'read_cycles': read_cycles,
|
|
677
|
+
'total_duration': time.time() - last_read_time + no_content_duration,
|
|
678
|
+
'process_type': handle.process_type
|
|
679
|
+
}
|
|
680
|
+
)
|
|
681
|
+
|
|
682
|
+
except Exception as e:
|
|
683
|
+
logger.error(f"Error in progressive read from session {session_id}: {e}")
|
|
684
|
+
return ToolResult(
|
|
685
|
+
success=False,
|
|
686
|
+
message=f"Error in progressive read from session: {str(e)}"
|
|
687
|
+
)
|
|
688
|
+
|
|
689
|
+
def send_input_then_get_progressive(
|
|
690
|
+
self,
|
|
691
|
+
session_id: str,
|
|
692
|
+
input_text: str,
|
|
693
|
+
read_timeout: Optional[int] = None,
|
|
694
|
+
max_bytes: Optional[int] = None,
|
|
695
|
+
expect_prompt: Optional[bool] = False,
|
|
696
|
+
prompt_regex: Optional[str] = r'>>> ?$'
|
|
697
|
+
) -> ToolResult:
|
|
698
|
+
"""
|
|
699
|
+
Send input to a session and then progressively read output with intelligent timeout handling.
|
|
700
|
+
|
|
701
|
+
This method sends input first, then waits for initial output and continues reading
|
|
702
|
+
as long as content is available, stopping only after 5 seconds of no new content.
|
|
703
|
+
|
|
704
|
+
Args:
|
|
705
|
+
session_id: Session ID
|
|
706
|
+
input_text: Text to send to stdin
|
|
707
|
+
read_timeout: Initial timeout for reading output (default: 3 seconds)
|
|
708
|
+
max_bytes: Maximum bytes to read in total
|
|
709
|
+
expect_prompt: Whether to wait for prompt before returning
|
|
710
|
+
prompt_regex: Regular expression to match prompt
|
|
711
|
+
|
|
712
|
+
Returns:
|
|
713
|
+
ToolResult with accumulated output
|
|
714
|
+
"""
|
|
715
|
+
with self._cleanup_lock:
|
|
716
|
+
handle = self.sessions.get(session_id)
|
|
717
|
+
if not handle:
|
|
718
|
+
return ToolResult(
|
|
719
|
+
success=False,
|
|
720
|
+
message=f"Session {session_id} not found"
|
|
721
|
+
)
|
|
722
|
+
|
|
723
|
+
# Update activity
|
|
724
|
+
handle.update_activity()
|
|
725
|
+
|
|
726
|
+
try:
|
|
727
|
+
# Check if session is still alive
|
|
728
|
+
if not handle.session.process.is_alive():
|
|
729
|
+
return ToolResult(
|
|
730
|
+
success=False,
|
|
731
|
+
message=f"Session {session_id} is no longer alive"
|
|
732
|
+
)
|
|
733
|
+
|
|
734
|
+
# Send input first
|
|
735
|
+
logger.debug(f"Session {session_id} ({handle.process_type}): Sending input: {input_text[:50]}...")
|
|
736
|
+
|
|
737
|
+
if handle.process_type == ProcessType.PEXPECT:
|
|
738
|
+
# Use pexpect-specific methods
|
|
739
|
+
if hasattr(handle.session.process, 'sendline'):
|
|
740
|
+
clean_input = input_text.rstrip('\n\r')
|
|
741
|
+
handle.session.process.sendline(clean_input)
|
|
742
|
+
else:
|
|
743
|
+
handle.session.process.write(input_text)
|
|
744
|
+
else:
|
|
745
|
+
handle.session.process.write(input_text)
|
|
746
|
+
|
|
747
|
+
# Progressive reading logic (similar to read_output_progressive)
|
|
748
|
+
initial_timeout = read_timeout or 3
|
|
749
|
+
accumulated_output = ""
|
|
750
|
+
total_bytes_read = 0
|
|
751
|
+
read_cycles = 0
|
|
752
|
+
no_content_duration = 0
|
|
753
|
+
last_read_time = time.time()
|
|
754
|
+
|
|
755
|
+
logger.debug(f"Session {session_id} ({handle.process_type}): Starting progressive read after input with initial timeout {initial_timeout}s")
|
|
756
|
+
|
|
757
|
+
while True:
|
|
758
|
+
read_cycles += 1
|
|
759
|
+
cycle_start_time = time.time()
|
|
760
|
+
|
|
761
|
+
# Read with current timeout
|
|
762
|
+
if expect_prompt and accumulated_output:
|
|
763
|
+
# If we already have some output and expect prompt, check if we found it
|
|
764
|
+
lines = accumulated_output.split('\n')
|
|
765
|
+
if lines and re.search(prompt_regex or r'>>> ?$', lines[-1]):
|
|
766
|
+
logger.debug(f"Session {session_id}: Found prompt pattern, stopping progressive read")
|
|
767
|
+
break
|
|
768
|
+
|
|
769
|
+
# Read a chunk
|
|
770
|
+
chunk = handle.session.process.read_output(timeout=initial_timeout)
|
|
771
|
+
|
|
772
|
+
if chunk and chunk.strip():
|
|
773
|
+
# Got content, reset no-content counter
|
|
774
|
+
accumulated_output += chunk
|
|
775
|
+
total_bytes_read += len(chunk)
|
|
776
|
+
no_content_duration = 0
|
|
777
|
+
last_read_time = time.time()
|
|
778
|
+
|
|
779
|
+
logger.debug(f"Session {session_id}: Read cycle {read_cycles}, got {len(chunk)} bytes, total: {total_bytes_read} bytes")
|
|
780
|
+
|
|
781
|
+
# Check max_bytes limit
|
|
782
|
+
if max_bytes and total_bytes_read >= max_bytes:
|
|
783
|
+
logger.debug(f"Session {session_id}: Reached max_bytes limit ({max_bytes}), stopping")
|
|
784
|
+
break
|
|
785
|
+
|
|
786
|
+
else:
|
|
787
|
+
# No content in this cycle
|
|
788
|
+
current_time = time.time()
|
|
789
|
+
no_content_duration += current_time - cycle_start_time
|
|
790
|
+
|
|
791
|
+
logger.debug(f"Session {session_id}: Read cycle {read_cycles}, no content for {no_content_duration:.1f}s")
|
|
792
|
+
|
|
793
|
+
# If we have some content and no new content for 5 seconds, stop
|
|
794
|
+
if accumulated_output and no_content_duration >= 5.0:
|
|
795
|
+
logger.debug(f"Session {session_id}: No new content for 5 seconds, stopping progressive read")
|
|
796
|
+
break
|
|
797
|
+
|
|
798
|
+
# If we have no content at all and waited initial timeout, stop
|
|
799
|
+
if not accumulated_output and no_content_duration >= initial_timeout:
|
|
800
|
+
logger.debug(f"Session {session_id}: No initial content after {initial_timeout}s, stopping")
|
|
801
|
+
break
|
|
802
|
+
|
|
803
|
+
# Brief pause between cycles to avoid excessive CPU usage
|
|
804
|
+
time.sleep(0.1)
|
|
805
|
+
|
|
806
|
+
# Clean the accumulated output
|
|
807
|
+
cleaned_output = clean_terminal_output(accumulated_output)
|
|
808
|
+
|
|
809
|
+
logger.debug(f"Session {session_id} ({handle.process_type}): Progressive read after input completed after {read_cycles} cycles, "
|
|
810
|
+
f"got {total_bytes_read} bytes raw output, {len(cleaned_output)} chars cleaned")
|
|
811
|
+
|
|
812
|
+
return ToolResult(
|
|
813
|
+
success=True,
|
|
814
|
+
message=f"Input sent and progressive output read completed ({read_cycles} cycles)",
|
|
815
|
+
content={
|
|
816
|
+
'session_id': session_id,
|
|
817
|
+
'output': cleaned_output,
|
|
818
|
+
'raw_output': accumulated_output,
|
|
819
|
+
'input_sent': input_text,
|
|
820
|
+
'bytes_read': total_bytes_read,
|
|
821
|
+
'cleaned_length': len(cleaned_output),
|
|
822
|
+
'read_cycles': read_cycles,
|
|
823
|
+
'total_duration': time.time() - last_read_time + no_content_duration,
|
|
824
|
+
'process_type': handle.process_type
|
|
825
|
+
}
|
|
826
|
+
)
|
|
827
|
+
|
|
828
|
+
except Exception as e:
|
|
829
|
+
logger.error(f"Error in send input then progressive read from session {session_id}: {e}")
|
|
830
|
+
return ToolResult(
|
|
831
|
+
success=False,
|
|
832
|
+
message=f"Error in send input then progressive read from session: {str(e)}"
|
|
833
|
+
)
|
|
834
|
+
|
|
835
|
+
def terminate_session(self, session_id: str, force: bool = False) -> ToolResult:
|
|
836
|
+
"""
|
|
837
|
+
Terminate a session.
|
|
838
|
+
|
|
839
|
+
Args:
|
|
840
|
+
session_id: Session ID
|
|
841
|
+
force: Whether to force termination
|
|
842
|
+
|
|
843
|
+
Returns:
|
|
844
|
+
ToolResult indicating success
|
|
845
|
+
"""
|
|
846
|
+
with self._cleanup_lock:
|
|
847
|
+
handle = self.sessions.pop(session_id, None)
|
|
848
|
+
if not handle:
|
|
849
|
+
return ToolResult(
|
|
850
|
+
success=False,
|
|
851
|
+
message=f"Session {session_id} not found"
|
|
852
|
+
)
|
|
853
|
+
|
|
854
|
+
try:
|
|
855
|
+
if force:
|
|
856
|
+
# Force termination - use terminate with grace_timeout=0
|
|
857
|
+
handle.session.process.terminate(grace_timeout=0.0)
|
|
858
|
+
else:
|
|
859
|
+
# Graceful termination
|
|
860
|
+
handle.session.terminate()
|
|
861
|
+
|
|
862
|
+
logger.info(f"Terminated session {session_id} ({handle.process_type}, force={force})")
|
|
863
|
+
|
|
864
|
+
return ToolResult(
|
|
865
|
+
success=True,
|
|
866
|
+
message=f"Session {session_id} terminated successfully",
|
|
867
|
+
content={
|
|
868
|
+
'session_id': session_id,
|
|
869
|
+
'process_type': handle.process_type,
|
|
870
|
+
'terminated': True,
|
|
871
|
+
'force': force
|
|
872
|
+
}
|
|
873
|
+
)
|
|
874
|
+
|
|
875
|
+
except Exception as e:
|
|
876
|
+
logger.error(f"Error terminating session {session_id}: {e}")
|
|
877
|
+
return ToolResult(
|
|
878
|
+
success=False,
|
|
879
|
+
message=f"Error terminating session: {str(e)}"
|
|
880
|
+
)
|
|
881
|
+
|
|
882
|
+
def get_session_info(self, session_id: str) -> Optional[Dict[str, Any]]:
|
|
883
|
+
"""Get information about a session."""
|
|
884
|
+
with self._cleanup_lock:
|
|
885
|
+
handle = self.sessions.get(session_id)
|
|
886
|
+
if handle:
|
|
887
|
+
return handle.get_stats()
|
|
888
|
+
return None
|
|
889
|
+
|
|
890
|
+
def list_sessions(self) -> List[Dict[str, Any]]:
|
|
891
|
+
"""List all active sessions with process type information."""
|
|
892
|
+
with self._cleanup_lock:
|
|
893
|
+
return [handle.get_stats() for handle in self.sessions.values()]
|
|
894
|
+
|
|
895
|
+
def get_process_type_stats(self) -> Dict[str, Any]:
|
|
896
|
+
"""Get statistics about process types in use."""
|
|
897
|
+
with self._cleanup_lock:
|
|
898
|
+
stats = {
|
|
899
|
+
'total_sessions': len(self.sessions),
|
|
900
|
+
'by_process_type': {},
|
|
901
|
+
'pexpect_available': PEXPECT_AVAILABLE,
|
|
902
|
+
'platform': platform.system()
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
for handle in self.sessions.values():
|
|
906
|
+
process_type = handle.process_type
|
|
907
|
+
if process_type not in stats['by_process_type']:
|
|
908
|
+
stats['by_process_type'][process_type] = 0
|
|
909
|
+
stats['by_process_type'][process_type] += 1
|
|
910
|
+
|
|
911
|
+
return stats
|
|
912
|
+
|
|
913
|
+
def cleanup_all(self):
|
|
914
|
+
"""Clean up all sessions."""
|
|
915
|
+
self._shutdown = True
|
|
916
|
+
|
|
917
|
+
with self._cleanup_lock:
|
|
918
|
+
for session_id, handle in list(self.sessions.items()):
|
|
919
|
+
try:
|
|
920
|
+
logger.info(f"Cleaning up session {session_id} ({handle.process_type})")
|
|
921
|
+
handle.session.terminate()
|
|
922
|
+
except Exception as e:
|
|
923
|
+
logger.error(f"Error terminating session {session_id} during cleanup: {e}")
|
|
924
|
+
|
|
925
|
+
self.sessions.clear()
|
|
926
|
+
|
|
927
|
+
# Clean up executor
|
|
928
|
+
try:
|
|
929
|
+
self.executor.cleanup()
|
|
930
|
+
except Exception as e:
|
|
931
|
+
logger.error(f"Error cleaning up executor: {e}")
|
|
932
|
+
|
|
933
|
+
logger.info("InteractiveSessionManager cleanup completed")
|
|
934
|
+
|
|
935
|
+
def _read_until_prompt(self, process, prompt_regex: str, timeout: int) -> str:
|
|
936
|
+
"""
|
|
937
|
+
Read output until a prompt pattern is found or timeout occurs (standard process).
|
|
938
|
+
|
|
939
|
+
Args:
|
|
940
|
+
process: The interactive process
|
|
941
|
+
prompt_regex: Regular expression to match prompt
|
|
942
|
+
timeout: Total timeout in seconds
|
|
943
|
+
|
|
944
|
+
Returns:
|
|
945
|
+
Accumulated output string
|
|
946
|
+
"""
|
|
947
|
+
import time
|
|
948
|
+
import re
|
|
949
|
+
|
|
950
|
+
start_time = time.time()
|
|
951
|
+
accumulated_output = ""
|
|
952
|
+
prompt_pattern = re.compile(prompt_regex, re.MULTILINE)
|
|
953
|
+
|
|
954
|
+
while time.time() - start_time < timeout:
|
|
955
|
+
# Read a chunk with short timeout
|
|
956
|
+
chunk = process.read_output(timeout=0.1)
|
|
957
|
+
if chunk:
|
|
958
|
+
accumulated_output += chunk
|
|
959
|
+
|
|
960
|
+
# Check if we found the prompt pattern
|
|
961
|
+
# Look for prompt at the end of the accumulated output
|
|
962
|
+
lines = accumulated_output.split('\n')
|
|
963
|
+
if len(lines) > 0:
|
|
964
|
+
last_line = lines[-1]
|
|
965
|
+
if prompt_pattern.search(last_line):
|
|
966
|
+
logger.debug(f"Found prompt pattern '{prompt_regex}' in: {repr(last_line)}")
|
|
967
|
+
break
|
|
968
|
+
else:
|
|
969
|
+
# No output available, sleep briefly
|
|
970
|
+
time.sleep(0.05)
|
|
971
|
+
|
|
972
|
+
return accumulated_output
|
|
973
|
+
|
|
974
|
+
def _read_until_prompt_pexpect(self, process, prompt_regex: str, timeout: int) -> str:
|
|
975
|
+
"""
|
|
976
|
+
Read output until a prompt pattern is found using pexpect expect functionality.
|
|
977
|
+
|
|
978
|
+
Args:
|
|
979
|
+
process: The pexpect process
|
|
980
|
+
prompt_regex: Regular expression to match prompt
|
|
981
|
+
timeout: Total timeout in seconds
|
|
982
|
+
|
|
983
|
+
Returns:
|
|
984
|
+
Accumulated output string
|
|
985
|
+
"""
|
|
986
|
+
try:
|
|
987
|
+
if hasattr(process, 'expect'):
|
|
988
|
+
# Use pexpect's built-in expect functionality
|
|
989
|
+
process.expect(prompt_regex, timeout=timeout)
|
|
990
|
+
# Return the output before the match
|
|
991
|
+
before = getattr(process, 'before', '')
|
|
992
|
+
after = getattr(process, 'after', '')
|
|
993
|
+
return (before or '') + (after or '')
|
|
994
|
+
else:
|
|
995
|
+
# Fallback to standard prompt reading
|
|
996
|
+
return self._read_until_prompt(process, prompt_regex, timeout)
|
|
997
|
+
except Exception as e:
|
|
998
|
+
logger.debug(f"pexpect expect failed, using fallback: {e}")
|
|
999
|
+
return self._read_until_prompt(process, prompt_regex, timeout)
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
# Global instance
|
|
1003
|
+
_session_manager = None
|
|
1004
|
+
_session_manager_lock = threading.Lock()
|
|
1005
|
+
|
|
1006
|
+
|
|
1007
|
+
def get_session_manager() -> InteractiveSessionManager:
|
|
1008
|
+
"""Get the global session manager instance."""
|
|
1009
|
+
global _session_manager
|
|
1010
|
+
if _session_manager is None:
|
|
1011
|
+
with _session_manager_lock:
|
|
1012
|
+
if _session_manager is None:
|
|
1013
|
+
_session_manager = InteractiveSessionManager()
|
|
1014
|
+
return _session_manager
|