octo-agent 0.11.2
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.
- checksums.yaml +7 -0
- data/.clacky/skills/commit/SKILL.md +423 -0
- data/.clacky/skills/gem-release/SKILL.md +199 -0
- data/.clacky/skills/gem-release/scripts/release.sh +304 -0
- data/.clacky/skills/oss-upload/SKILL.md +47 -0
- data/.octorules +106 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/CHANGELOG.md +76 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/CONTRIBUTING.md +92 -0
- data/Dockerfile +28 -0
- data/LICENSE.txt +22 -0
- data/POSITIONING.md +46 -0
- data/README.md +134 -0
- data/README_CN.md +134 -0
- data/Rakefile +34 -0
- data/benchmark/fixtures/sample_project/Gemfile +3 -0
- data/benchmark/fixtures/sample_project/lib/api_handler.rb +32 -0
- data/benchmark/fixtures/sample_project/lib/order_calculator.rb +23 -0
- data/benchmark/fixtures/sample_project/lib/user_renderer.rb +20 -0
- data/benchmark/fixtures/sample_project/spec/order_calculator_spec.rb +20 -0
- data/benchmark/results/EVALUATION_REPORT.md +165 -0
- data/benchmark/results/baseline_20260511_174424.json +128 -0
- data/benchmark/results/report_20260511_175256.json +271 -0
- data/benchmark/results/report_20260511_175444.json +271 -0
- data/benchmark/results/treatment_20260511_175103.json +130 -0
- data/benchmark/runner.rb +441 -0
- data/bin/octo +7 -0
- data/docs/agent-first-ui-design.md +77 -0
- data/docs/billing-system.md +318 -0
- data/docs/channel-architecture.md +235 -0
- data/docs/engineering-article.md +343 -0
- data/docs/session-skill-invocation.md +69 -0
- data/docs/time_machine_design.md +247 -0
- data/docs/ui2-architecture.md +124 -0
- data/homebrew/README.md +96 -0
- data/homebrew/openocto.rb +24 -0
- data/lib/octo/agent/hook_manager.rb +61 -0
- data/lib/octo/agent/llm_caller.rb +800 -0
- data/lib/octo/agent/memory_updater.rb +246 -0
- data/lib/octo/agent/message_compressor.rb +225 -0
- data/lib/octo/agent/message_compressor_helper.rb +869 -0
- data/lib/octo/agent/next_message_suggester.rb +215 -0
- data/lib/octo/agent/session_serializer.rb +685 -0
- data/lib/octo/agent/skill_auto_creator.rb +114 -0
- data/lib/octo/agent/skill_evolution.rb +61 -0
- data/lib/octo/agent/skill_manager.rb +466 -0
- data/lib/octo/agent/skill_reflector.rb +89 -0
- data/lib/octo/agent/system_prompt_builder.rb +101 -0
- data/lib/octo/agent/time_machine.rb +214 -0
- data/lib/octo/agent/tool_executor.rb +454 -0
- data/lib/octo/agent/tool_registry.rb +150 -0
- data/lib/octo/agent.rb +2180 -0
- data/lib/octo/agent_config.rb +989 -0
- data/lib/octo/agent_profile.rb +112 -0
- data/lib/octo/anthropic_stream_aggregator.rb +137 -0
- data/lib/octo/background_task_registry.rb +324 -0
- data/lib/octo/banner.rb +34 -0
- data/lib/octo/bedrock_stream_aggregator.rb +137 -0
- data/lib/octo/block_font.rb +331 -0
- data/lib/octo/cli.rb +968 -0
- data/lib/octo/client.rb +623 -0
- data/lib/octo/default_agents/SOUL.md +3 -0
- data/lib/octo/default_agents/USER.md +1 -0
- data/lib/octo/default_agents/base_prompt.md +66 -0
- data/lib/octo/default_agents/coding/profile.yml +2 -0
- data/lib/octo/default_agents/coding/system_prompt.md +67 -0
- data/lib/octo/default_agents/general/profile.yml +2 -0
- data/lib/octo/default_agents/general/system_prompt.md +16 -0
- data/lib/octo/default_parsers/doc_parser.rb +69 -0
- data/lib/octo/default_parsers/docx_parser.rb +188 -0
- data/lib/octo/default_parsers/pdf_parser.rb +120 -0
- data/lib/octo/default_parsers/pdf_parser_ocr.py +103 -0
- data/lib/octo/default_parsers/pdf_parser_plumber.py +62 -0
- data/lib/octo/default_parsers/pptx_parser.rb +140 -0
- data/lib/octo/default_parsers/xlsx_parser.rb +121 -0
- data/lib/octo/default_skills/browser-setup/SKILL.md +426 -0
- data/lib/octo/default_skills/channel-manager/SKILL.md +623 -0
- data/lib/octo/default_skills/channel-manager/dingtalk_setup.rb +191 -0
- data/lib/octo/default_skills/channel-manager/discord_setup.rb +199 -0
- data/lib/octo/default_skills/channel-manager/feishu_setup.rb +574 -0
- data/lib/octo/default_skills/channel-manager/import_lark_skills.rb +97 -0
- data/lib/octo/default_skills/channel-manager/install_feishu_skills.rb +105 -0
- data/lib/octo/default_skills/channel-manager/weixin_setup.rb +274 -0
- data/lib/octo/default_skills/code-explorer/SKILL.md +36 -0
- data/lib/octo/default_skills/cron-task-creator/SKILL.md +257 -0
- data/lib/octo/default_skills/cron-task-creator/evals/evals.json +38 -0
- data/lib/octo/default_skills/onboard/SKILL.md +578 -0
- data/lib/octo/default_skills/onboard/scripts/import_external_skills.rb +413 -0
- data/lib/octo/default_skills/onboard/scripts/install_builtin_skills.rb +97 -0
- data/lib/octo/default_skills/persist-memory/SKILL.md +59 -0
- data/lib/octo/default_skills/personal-website/SKILL.md +113 -0
- data/lib/octo/default_skills/personal-website/publish.rb +235 -0
- data/lib/octo/default_skills/product-help/SKILL.md +123 -0
- data/lib/octo/default_skills/product-help/docs/agent-config.md +74 -0
- data/lib/octo/default_skills/product-help/docs/best-practices.md +49 -0
- data/lib/octo/default_skills/product-help/docs/browser-tool.md +53 -0
- data/lib/octo/default_skills/product-help/docs/built-in-skills.md +43 -0
- data/lib/octo/default_skills/product-help/docs/cli-reference.md +82 -0
- data/lib/octo/default_skills/product-help/docs/create-your-first-skill.md +47 -0
- data/lib/octo/default_skills/product-help/docs/faq.md +98 -0
- data/lib/octo/default_skills/product-help/docs/how-to-use-a-skill.md +58 -0
- data/lib/octo/default_skills/product-help/docs/installation.md +59 -0
- data/lib/octo/default_skills/product-help/docs/memory-system.md +61 -0
- data/lib/octo/default_skills/product-help/docs/octorules.md +62 -0
- data/lib/octo/default_skills/product-help/docs/session-management.md +63 -0
- data/lib/octo/default_skills/product-help/docs/skill-basics.md +55 -0
- data/lib/octo/default_skills/product-help/docs/skill-frontmatter.md +61 -0
- data/lib/octo/default_skills/product-help/docs/web-server.md +49 -0
- data/lib/octo/default_skills/product-help/docs/what-is-octo.md +37 -0
- data/lib/octo/default_skills/product-help/docs/windows-installation.md +36 -0
- data/lib/octo/default_skills/product-help/docs/writing-tips.md +53 -0
- data/lib/octo/default_skills/recall-memory/SKILL.md +65 -0
- data/lib/octo/default_skills/skill-add/SKILL.md +59 -0
- data/lib/octo/default_skills/skill-add/scripts/install_from_zip.rb +295 -0
- data/lib/octo/default_skills/skill-creator/SKILL.md +602 -0
- data/lib/octo/default_skills/skill-creator/agents/analyzer.md +274 -0
- data/lib/octo/default_skills/skill-creator/agents/comparator.md +202 -0
- data/lib/octo/default_skills/skill-creator/agents/grader.md +223 -0
- data/lib/octo/default_skills/skill-creator/eval-viewer/generate_review.py +471 -0
- data/lib/octo/default_skills/skill-creator/eval-viewer/viewer.html +1325 -0
- data/lib/octo/default_skills/skill-creator/references/schemas.md +430 -0
- data/lib/octo/default_skills/skill-creator/scripts/__init__.py +0 -0
- data/lib/octo/default_skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- data/lib/octo/default_skills/skill-creator/scripts/generate_report.py +326 -0
- data/lib/octo/default_skills/skill-creator/scripts/improve_description.py +310 -0
- data/lib/octo/default_skills/skill-creator/scripts/quick_validate.py +103 -0
- data/lib/octo/default_skills/skill-creator/scripts/run_eval.py +317 -0
- data/lib/octo/default_skills/skill-creator/scripts/run_loop.py +331 -0
- data/lib/octo/default_skills/skill-creator/scripts/utils.py +47 -0
- data/lib/octo/default_skills/skill-creator/scripts/validate_skill_frontmatter.rb +143 -0
- data/lib/octo/idle_compression_timer.rb +115 -0
- data/lib/octo/json_ui_controller.rb +204 -0
- data/lib/octo/message_format/anthropic.rb +409 -0
- data/lib/octo/message_format/bedrock.rb +361 -0
- data/lib/octo/message_format/open_ai.rb +222 -0
- data/lib/octo/message_history.rb +373 -0
- data/lib/octo/openai_stream_aggregator.rb +130 -0
- data/lib/octo/plain_ui_controller.rb +166 -0
- data/lib/octo/providers.rb +534 -0
- data/lib/octo/server/browser_manager.rb +397 -0
- data/lib/octo/server/channel/adapters/base.rb +82 -0
- data/lib/octo/server/channel/adapters/dingtalk/adapter.rb +314 -0
- data/lib/octo/server/channel/adapters/dingtalk/api_client.rb +391 -0
- data/lib/octo/server/channel/adapters/dingtalk/stream_client.rb +203 -0
- data/lib/octo/server/channel/adapters/discord/adapter.rb +229 -0
- data/lib/octo/server/channel/adapters/discord/api_client.rb +107 -0
- data/lib/octo/server/channel/adapters/discord/gateway_client.rb +270 -0
- data/lib/octo/server/channel/adapters/feishu/adapter.rb +320 -0
- data/lib/octo/server/channel/adapters/feishu/bot.rb +478 -0
- data/lib/octo/server/channel/adapters/feishu/file_processor.rb +36 -0
- data/lib/octo/server/channel/adapters/feishu/message_parser.rb +129 -0
- data/lib/octo/server/channel/adapters/feishu/ws_client.rb +423 -0
- data/lib/octo/server/channel/adapters/telegram/adapter.rb +375 -0
- data/lib/octo/server/channel/adapters/telegram/api_client.rb +205 -0
- data/lib/octo/server/channel/adapters/wecom/adapter.rb +148 -0
- data/lib/octo/server/channel/adapters/wecom/media_downloader.rb +115 -0
- data/lib/octo/server/channel/adapters/wecom/ws_client.rb +395 -0
- data/lib/octo/server/channel/adapters/weixin/adapter.rb +692 -0
- data/lib/octo/server/channel/adapters/weixin/api_client.rb +402 -0
- data/lib/octo/server/channel/channel_config.rb +178 -0
- data/lib/octo/server/channel/channel_manager.rb +468 -0
- data/lib/octo/server/channel/channel_ui_controller.rb +224 -0
- data/lib/octo/server/channel.rb +33 -0
- data/lib/octo/server/discover.rb +77 -0
- data/lib/octo/server/epipe_safe_io.rb +105 -0
- data/lib/octo/server/http_server.rb +3554 -0
- data/lib/octo/server/scheduler.rb +317 -0
- data/lib/octo/server/server_master.rb +325 -0
- data/lib/octo/server/session_registry.rb +431 -0
- data/lib/octo/server/web_ui_controller.rb +487 -0
- data/lib/octo/session_manager.rb +385 -0
- data/lib/octo/skill.rb +466 -0
- data/lib/octo/skill_loader.rb +328 -0
- data/lib/octo/tools/base.rb +118 -0
- data/lib/octo/tools/browser.rb +625 -0
- data/lib/octo/tools/edit.rb +165 -0
- data/lib/octo/tools/file_reader.rb +549 -0
- data/lib/octo/tools/glob.rb +162 -0
- data/lib/octo/tools/grep.rb +356 -0
- data/lib/octo/tools/invoke_skill.rb +96 -0
- data/lib/octo/tools/list_tasks.rb +54 -0
- data/lib/octo/tools/redo_task.rb +41 -0
- data/lib/octo/tools/request_user_feedback.rb +84 -0
- data/lib/octo/tools/security.rb +333 -0
- data/lib/octo/tools/terminal/output_cleaner.rb +63 -0
- data/lib/octo/tools/terminal/persistent_session.rb +268 -0
- data/lib/octo/tools/terminal/safe_rm.sh +106 -0
- data/lib/octo/tools/terminal/session_manager.rb +213 -0
- data/lib/octo/tools/terminal.rb +1828 -0
- data/lib/octo/tools/todo_manager.rb +374 -0
- data/lib/octo/tools/trash_manager.rb +388 -0
- data/lib/octo/tools/undo_task.rb +35 -0
- data/lib/octo/tools/web_fetch.rb +242 -0
- data/lib/octo/tools/web_search.rb +260 -0
- data/lib/octo/tools/write.rb +77 -0
- data/lib/octo/ui2/block_font.rb +10 -0
- data/lib/octo/ui2/components/base_component.rb +163 -0
- data/lib/octo/ui2/components/command_suggestions.rb +290 -0
- data/lib/octo/ui2/components/common_component.rb +96 -0
- data/lib/octo/ui2/components/inline_input.rb +226 -0
- data/lib/octo/ui2/components/input_area.rb +1338 -0
- data/lib/octo/ui2/components/message_component.rb +99 -0
- data/lib/octo/ui2/components/modal_component.rb +419 -0
- data/lib/octo/ui2/components/todo_area.rb +149 -0
- data/lib/octo/ui2/components/tool_component.rb +107 -0
- data/lib/octo/ui2/components/welcome_banner.rb +139 -0
- data/lib/octo/ui2/layout_manager.rb +807 -0
- data/lib/octo/ui2/line_editor.rb +363 -0
- data/lib/octo/ui2/markdown_renderer.rb +100 -0
- data/lib/octo/ui2/output_buffer.rb +370 -0
- data/lib/octo/ui2/progress_handle.rb +362 -0
- data/lib/octo/ui2/progress_indicator.rb +55 -0
- data/lib/octo/ui2/screen_buffer.rb +273 -0
- data/lib/octo/ui2/terminal_detector.rb +119 -0
- data/lib/octo/ui2/theme_manager.rb +85 -0
- data/lib/octo/ui2/themes/base_theme.rb +105 -0
- data/lib/octo/ui2/themes/hacker_theme.rb +62 -0
- data/lib/octo/ui2/themes/minimal_theme.rb +56 -0
- data/lib/octo/ui2/thinking_verbs.rb +26 -0
- data/lib/octo/ui2/ui_controller.rb +1625 -0
- data/lib/octo/ui2/view_renderer.rb +177 -0
- data/lib/octo/ui2.rb +40 -0
- data/lib/octo/ui_interface.rb +154 -0
- data/lib/octo/utils/arguments_parser.rb +191 -0
- data/lib/octo/utils/browser_detector.rb +195 -0
- data/lib/octo/utils/encoding.rb +92 -0
- data/lib/octo/utils/environment_detector.rb +140 -0
- data/lib/octo/utils/file_ignore_helper.rb +170 -0
- data/lib/octo/utils/file_processor.rb +601 -0
- data/lib/octo/utils/gitignore_parser.rb +154 -0
- data/lib/octo/utils/limit_stack.rb +152 -0
- data/lib/octo/utils/logger.rb +124 -0
- data/lib/octo/utils/login_shell.rb +72 -0
- data/lib/octo/utils/model_pricing.rb +646 -0
- data/lib/octo/utils/parser_manager.rb +165 -0
- data/lib/octo/utils/path_helper.rb +15 -0
- data/lib/octo/utils/scripts_manager.rb +59 -0
- data/lib/octo/utils/string_matcher.rb +158 -0
- data/lib/octo/utils/trash_directory.rb +112 -0
- data/lib/octo/utils/workspace_rules.rb +46 -0
- data/lib/octo/version.rb +5 -0
- data/lib/octo/web/app.css +7141 -0
- data/lib/octo/web/app.js +543 -0
- data/lib/octo/web/apple-touch-icon.png +0 -0
- data/lib/octo/web/auth.js +150 -0
- data/lib/octo/web/channels.js +276 -0
- data/lib/octo/web/datepicker.js +205 -0
- data/lib/octo/web/favicon.png +0 -0
- data/lib/octo/web/i18n.js +1073 -0
- data/lib/octo/web/icon-512.png +0 -0
- data/lib/octo/web/icon-dark.svg +25 -0
- data/lib/octo/web/icon.svg +29 -0
- data/lib/octo/web/index.html +871 -0
- data/lib/octo/web/marked.min.js +69 -0
- data/lib/octo/web/onboard.js +491 -0
- data/lib/octo/web/profile.js +442 -0
- data/lib/octo/web/sessions.js +4421 -0
- data/lib/octo/web/settings.js +913 -0
- data/lib/octo/web/sidebar.js +32 -0
- data/lib/octo/web/skills.js +885 -0
- data/lib/octo/web/tasks.js +297 -0
- data/lib/octo/web/theme.js +105 -0
- data/lib/octo/web/trash.js +343 -0
- data/lib/octo/web/vendor/hljs/highlight.min.js +1244 -0
- data/lib/octo/web/vendor/hljs/hljs-theme.css +95 -0
- data/lib/octo/web/vendor/katex/auto-render.min.js +1 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_AMS-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Caligraphic-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Fraktur-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-BoldItalic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Italic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Main-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-BoldItalic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Math-Italic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Bold.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Italic.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_SansSerif-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Script-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size1-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size2-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size3-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Size4-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/fonts/KaTeX_Typewriter-Regular.woff2 +0 -0
- data/lib/octo/web/vendor/katex/katex.min.css +1 -0
- data/lib/octo/web/vendor/katex/katex.min.js +1 -0
- data/lib/octo/web/version.js +449 -0
- data/lib/octo/web/weixin-qr.html +209 -0
- data/lib/octo/web/ws-dispatcher.js +357 -0
- data/lib/octo/web/ws.js +128 -0
- data/lib/octo.rb +145 -0
- data/scripts/build/build.sh +329 -0
- data/scripts/build/lib/apt.sh +56 -0
- data/scripts/build/lib/brew.sh +89 -0
- data/scripts/build/lib/colors.sh +17 -0
- data/scripts/build/lib/gem.sh +95 -0
- data/scripts/build/lib/mise.sh +125 -0
- data/scripts/build/lib/network.sh +157 -0
- data/scripts/build/lib/os.sh +57 -0
- data/scripts/build/lib/shell.sh +37 -0
- data/scripts/build/src/install.sh.cc +174 -0
- data/scripts/build/src/install_browser.sh.cc +101 -0
- data/scripts/build/src/install_full.sh.cc +290 -0
- data/scripts/build/src/install_rails_deps.sh.cc +145 -0
- data/scripts/build/src/install_system_deps.sh.cc +123 -0
- data/scripts/build/src/uninstall.sh.cc +101 -0
- data/scripts/install.ps1 +532 -0
- data/scripts/install.sh +567 -0
- data/scripts/install_browser.sh +479 -0
- data/scripts/install_full.sh +838 -0
- data/scripts/install_rails_deps.sh +746 -0
- data/scripts/install_system_deps.sh +518 -0
- data/scripts/uninstall.sh +287 -0
- data/sig/octo.rbs +4 -0
- metadata +614 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Octo
|
|
4
|
+
class Agent
|
|
5
|
+
# Tool execution and permission management
|
|
6
|
+
# Handles tool confirmation, preview, and result building
|
|
7
|
+
module ToolExecutor
|
|
8
|
+
# Check if a tool should be auto-executed based on permission mode
|
|
9
|
+
# @param tool_name [String] Name of the tool
|
|
10
|
+
# @param tool_params [Hash, String] Tool parameters
|
|
11
|
+
# @return [Boolean] true if should auto-execute
|
|
12
|
+
def should_auto_execute?(tool_name, tool_params = {})
|
|
13
|
+
case @config.permission_mode
|
|
14
|
+
when :auto_approve, :confirm_all
|
|
15
|
+
# Both modes auto-execute all file/shell tools without confirmation.
|
|
16
|
+
# The difference is only in request_user_feedback handling:
|
|
17
|
+
# auto_approve → no human present, inject auto_reply
|
|
18
|
+
# confirm_all → human present, truly wait for user input
|
|
19
|
+
true
|
|
20
|
+
when :confirm_safes
|
|
21
|
+
# Use Security module to check auto-execution safety
|
|
22
|
+
is_safe_operation?(tool_name, tool_params)
|
|
23
|
+
else
|
|
24
|
+
false
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Check if an operation is considered safe for auto-execution
|
|
29
|
+
# @param tool_name [String] Name of the tool
|
|
30
|
+
# @param tool_params [Hash, String] Tool parameters
|
|
31
|
+
# @return [Boolean] true if safe operation
|
|
32
|
+
def is_safe_operation?(tool_name, tool_params = {})
|
|
33
|
+
# For terminal commands, defer to Security layer for the verdict.
|
|
34
|
+
if tool_name.to_s.downcase == 'terminal'
|
|
35
|
+
params = tool_params.is_a?(String) ? JSON.parse(tool_params) : tool_params
|
|
36
|
+
command = params[:command] || params['command']
|
|
37
|
+
# No command = session_id continuation / kill / action → safe by default.
|
|
38
|
+
return true unless command
|
|
39
|
+
|
|
40
|
+
return Octo::Tools::Security.command_safe_for_auto_execution?(command)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
if tool_name.to_s.downcase == 'edit' || tool_name.to_s.downcase == 'write'
|
|
44
|
+
return false
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
true
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Request user confirmation for tool execution
|
|
51
|
+
# Shows preview and returns approval status
|
|
52
|
+
# @param call [Hash] Tool call with :name and :arguments
|
|
53
|
+
# @return [Hash] { approved: Boolean, feedback: String, system_injected: Boolean }
|
|
54
|
+
def confirm_tool_use?(call)
|
|
55
|
+
# Show preview first and check for errors
|
|
56
|
+
preview_error = show_tool_preview(call)
|
|
57
|
+
|
|
58
|
+
# If preview detected an error, auto-deny and provide feedback
|
|
59
|
+
if preview_error && preview_error[:error]
|
|
60
|
+
feedback = build_preview_error_feedback(call[:name], preview_error)
|
|
61
|
+
return { approved: false, feedback: feedback, system_injected: true }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Request confirmation via UI
|
|
65
|
+
if @ui
|
|
66
|
+
prompt_text = format_tool_prompt(call)
|
|
67
|
+
result = @ui.request_confirmation(prompt_text, default: true)
|
|
68
|
+
|
|
69
|
+
case result
|
|
70
|
+
when true
|
|
71
|
+
{ approved: true, feedback: nil }
|
|
72
|
+
when false, nil
|
|
73
|
+
# User denied - add visual marker based on tool type
|
|
74
|
+
tool_name_capitalized = call[:name].capitalize
|
|
75
|
+
@ui&.show_info(" ↳ #{tool_name_capitalized} cancelled", prefix_newline: false)
|
|
76
|
+
{ approved: false, feedback: nil }
|
|
77
|
+
else
|
|
78
|
+
# String feedback - also add visual marker
|
|
79
|
+
tool_name_capitalized = call[:name].capitalize
|
|
80
|
+
@ui&.show_info(" ↳ #{tool_name_capitalized} cancelled", prefix_newline: false)
|
|
81
|
+
{ approved: false, feedback: result.to_s }
|
|
82
|
+
end
|
|
83
|
+
else
|
|
84
|
+
# Fallback: auto-approve if no UI
|
|
85
|
+
{ approved: true, feedback: nil }
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Show preview for tool execution
|
|
90
|
+
# @param call [Hash] Tool call with :name and :arguments
|
|
91
|
+
# @return [Hash, nil] Error information if preview detected issues
|
|
92
|
+
def show_tool_preview(call)
|
|
93
|
+
return nil unless @ui
|
|
94
|
+
|
|
95
|
+
begin
|
|
96
|
+
args = JSON.parse(call[:arguments], symbolize_names: true)
|
|
97
|
+
|
|
98
|
+
preview_error = nil
|
|
99
|
+
case call[:name]
|
|
100
|
+
when "write"
|
|
101
|
+
preview_error = show_write_preview(args)
|
|
102
|
+
when "edit"
|
|
103
|
+
preview_error = show_edit_preview(args)
|
|
104
|
+
# Shell and other tools don't need special preview
|
|
105
|
+
# They will be shown via show_tool_call in the main flow
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
preview_error
|
|
109
|
+
rescue JSON::ParserError
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Format tool call for user confirmation prompt
|
|
115
|
+
# @param call [Hash] Tool call with :name and :arguments
|
|
116
|
+
# @return [String] Formatted prompt text
|
|
117
|
+
def format_tool_prompt(call)
|
|
118
|
+
begin
|
|
119
|
+
args = JSON.parse(call[:arguments], symbolize_names: true)
|
|
120
|
+
|
|
121
|
+
# Try to use tool's format_call method for better formatting
|
|
122
|
+
tool = @tool_registry.get(call[:name]) rescue nil
|
|
123
|
+
if tool
|
|
124
|
+
formatted = tool.format_call(args) rescue nil
|
|
125
|
+
return formatted if formatted
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Fallback to manual formatting for common tools
|
|
129
|
+
case call[:name]
|
|
130
|
+
when "edit"
|
|
131
|
+
path = args[:path] || args[:file_path]
|
|
132
|
+
filename = Utils::PathHelper.safe_basename(path)
|
|
133
|
+
"Edit(#{filename})"
|
|
134
|
+
when "write"
|
|
135
|
+
filename = Utils::PathHelper.safe_basename(args[:path])
|
|
136
|
+
if args[:path] && File.exist?(args[:path])
|
|
137
|
+
"Write(#{filename}) - overwrite existing"
|
|
138
|
+
else
|
|
139
|
+
"Write(#{filename}) - create new"
|
|
140
|
+
end
|
|
141
|
+
when "terminal"
|
|
142
|
+
cmd = args[:command] || ''
|
|
143
|
+
display_cmd = cmd.length > 30 ? "#{cmd[0..27]}..." : cmd
|
|
144
|
+
"terminal(\"#{display_cmd}\")"
|
|
145
|
+
else
|
|
146
|
+
"Allow #{call[:name]}"
|
|
147
|
+
end
|
|
148
|
+
rescue JSON::ParserError
|
|
149
|
+
"Allow #{call[:name]}"
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Build success result for tool execution
|
|
154
|
+
# @param call [Hash] Tool call
|
|
155
|
+
# @param result [Object] Tool execution result
|
|
156
|
+
# @return [Hash] Formatted result for LLM
|
|
157
|
+
def build_success_result(call, result)
|
|
158
|
+
# Try to get tool instance to use its format_result_for_llm method
|
|
159
|
+
tool = @tool_registry.get(call[:name]) rescue nil
|
|
160
|
+
|
|
161
|
+
formatted_result = if tool && tool.respond_to?(:format_result_for_llm)
|
|
162
|
+
# Tool provides a custom LLM-friendly format
|
|
163
|
+
tool.format_result_for_llm(result)
|
|
164
|
+
else
|
|
165
|
+
# Fallback: use the original result
|
|
166
|
+
result
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Inject TODO reminder for non-todo_manager tools
|
|
170
|
+
formatted_result = inject_todo_reminder(call[:name], formatted_result)
|
|
171
|
+
|
|
172
|
+
# Extract image_inject sidecar before building the tool content string.
|
|
173
|
+
# image_inject carries the base64 payload that must be delivered as a
|
|
174
|
+
# follow-up `role:"user"` message (OpenAI/OpenRouter/Gemini only accept
|
|
175
|
+
# image_url blocks in user messages, not in tool messages).
|
|
176
|
+
# Strip it from the content sent to the API so it isn't tokenised as text.
|
|
177
|
+
image_inject = nil
|
|
178
|
+
if formatted_result.is_a?(Hash) && formatted_result[:image_inject]
|
|
179
|
+
image_inject = formatted_result[:image_inject]
|
|
180
|
+
formatted_result = formatted_result.reject { |k, _| k == :image_inject }
|
|
181
|
+
if formatted_result[:content_string]
|
|
182
|
+
formatted_result = formatted_result[:content_string]
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# If the tool returned a plain string, use it directly (avoids double-escaping).
|
|
187
|
+
# If it returned an Array (e.g. multipart vision blocks with image + text),
|
|
188
|
+
# pass it through as-is so format_tool_results can send it to the API.
|
|
189
|
+
# Otherwise JSON-encode Hash/other values.
|
|
190
|
+
content = if formatted_result.is_a?(String)
|
|
191
|
+
formatted_result
|
|
192
|
+
elsif formatted_result.is_a?(Array)
|
|
193
|
+
formatted_result
|
|
194
|
+
else
|
|
195
|
+
JSON.generate(formatted_result)
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
result = { id: call[:id], content: content }
|
|
199
|
+
result[:image_inject] = image_inject if image_inject
|
|
200
|
+
result
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Build error result for tool execution
|
|
204
|
+
# @param call [Hash] Tool call
|
|
205
|
+
# @param error_message [String] Error message
|
|
206
|
+
# @return [Hash] Formatted error result
|
|
207
|
+
def build_error_result(call, error_message)
|
|
208
|
+
{
|
|
209
|
+
id: call[:id],
|
|
210
|
+
content: JSON.generate({ error: error_message })
|
|
211
|
+
}
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Build denied result when user denies tool execution
|
|
215
|
+
# @param call [Hash] Tool call
|
|
216
|
+
# @param user_feedback [String, nil] User's feedback message
|
|
217
|
+
# @param system_injected [Boolean] Whether this is a system-generated denial
|
|
218
|
+
# @return [Hash] Formatted denial result
|
|
219
|
+
def build_denied_result(call, user_feedback = nil, system_injected = false)
|
|
220
|
+
if system_injected
|
|
221
|
+
# System-generated feedback (e.g., from preview errors)
|
|
222
|
+
tool_content = {
|
|
223
|
+
error: "Tool #{call[:name]} denied: #{user_feedback}",
|
|
224
|
+
system_injected: true
|
|
225
|
+
}
|
|
226
|
+
else
|
|
227
|
+
# User manually denied or provided feedback
|
|
228
|
+
# Clearly state the action was NOT performed so the LLM knows the change did not happen
|
|
229
|
+
message = if user_feedback && !user_feedback.empty?
|
|
230
|
+
"Tool use denied by user. This action was NOT performed. User feedback: #{user_feedback}"
|
|
231
|
+
else
|
|
232
|
+
"Tool use denied by user. This action was NOT performed."
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
tool_content = {
|
|
236
|
+
error: message,
|
|
237
|
+
action_performed: false,
|
|
238
|
+
user_feedback: user_feedback
|
|
239
|
+
}
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
{
|
|
243
|
+
id: call[:id],
|
|
244
|
+
content: JSON.generate(tool_content)
|
|
245
|
+
}
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# Check if a tool is potentially slow and should show progress
|
|
249
|
+
# @param tool_name [String] Name of the tool
|
|
250
|
+
# @param args [Hash] Tool arguments
|
|
251
|
+
# @return [Boolean] true if tool is potentially slow
|
|
252
|
+
private def potentially_slow_tool?(tool_name, args)
|
|
253
|
+
case tool_name.to_s.downcase
|
|
254
|
+
when 'terminal'
|
|
255
|
+
# Check if the command is a slow command
|
|
256
|
+
command = args[:command] || args['command']
|
|
257
|
+
return false unless command
|
|
258
|
+
|
|
259
|
+
# List of slow command patterns
|
|
260
|
+
slow_patterns = [
|
|
261
|
+
/bundle\s+(install|exec\s+rspec|exec\s+rake)/,
|
|
262
|
+
/npm\s+(install|run\s+test|run\s+build)/,
|
|
263
|
+
/yarn\s+(install|test|build)/,
|
|
264
|
+
/pnpm\s+install/,
|
|
265
|
+
/cargo\s+(build|test)/,
|
|
266
|
+
/go\s+(build|test)/,
|
|
267
|
+
/make\s+(test|build)/,
|
|
268
|
+
/pytest/,
|
|
269
|
+
/jest/,
|
|
270
|
+
/sleep\s+\d+/
|
|
271
|
+
]
|
|
272
|
+
|
|
273
|
+
slow_patterns.any? { |pattern| command.match?(pattern) }
|
|
274
|
+
when 'web_fetch', 'web_search'
|
|
275
|
+
true
|
|
276
|
+
else
|
|
277
|
+
false
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
private def build_tool_progress_message(tool_name, args)
|
|
282
|
+
case tool_name.to_s.downcase
|
|
283
|
+
when 'terminal'
|
|
284
|
+
"Running command"
|
|
285
|
+
when 'web_fetch'
|
|
286
|
+
"Fetching web page"
|
|
287
|
+
when 'web_search'
|
|
288
|
+
"Searching web"
|
|
289
|
+
else
|
|
290
|
+
"Executing #{tool_name}"
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Inject TODO reminder into tool results for non-todo_manager tools
|
|
295
|
+
# This helps AI remember to mark TODOs as complete after executing tasks
|
|
296
|
+
# @param tool_name [String] Name of the tool
|
|
297
|
+
# @param result [Object] Tool execution result
|
|
298
|
+
# @return [Object] Result with optional TODO reminder
|
|
299
|
+
private def inject_todo_reminder(tool_name, result)
|
|
300
|
+
# Skip injection for todo_manager tool itself to avoid redundancy
|
|
301
|
+
return result if tool_name == "todo_manager"
|
|
302
|
+
|
|
303
|
+
# Get pending TODOs
|
|
304
|
+
todo_tool = @tool_registry.get("todo_manager")
|
|
305
|
+
return result unless todo_tool
|
|
306
|
+
|
|
307
|
+
pending_todos = begin
|
|
308
|
+
todo_result = todo_tool.execute(action: "list", todos_storage: @todos)
|
|
309
|
+
if todo_result.is_a?(Hash) && todo_result[:todos]
|
|
310
|
+
todo_result[:todos].select { |t| t[:status] == "pending" }
|
|
311
|
+
else
|
|
312
|
+
[]
|
|
313
|
+
end
|
|
314
|
+
rescue
|
|
315
|
+
[]
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# Only inject reminder if there are pending TODOs
|
|
319
|
+
return result unless pending_todos && !pending_todos.empty?
|
|
320
|
+
|
|
321
|
+
# Create a friendly reminder message
|
|
322
|
+
reminder = "\n\n📋 REMINDER: You have #{pending_todos.length} pending TODO(s). " \
|
|
323
|
+
"After completing each task, remember to mark it as complete using " \
|
|
324
|
+
"todo_manager with action 'complete' and the task id."
|
|
325
|
+
|
|
326
|
+
# Inject reminder based on result type
|
|
327
|
+
case result
|
|
328
|
+
when String
|
|
329
|
+
result + reminder
|
|
330
|
+
when Hash
|
|
331
|
+
result.merge({ _todo_reminder: reminder.strip })
|
|
332
|
+
when Array
|
|
333
|
+
result + [{ _todo_reminder: reminder.strip }]
|
|
334
|
+
else
|
|
335
|
+
result
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Build feedback message from preview error
|
|
340
|
+
# @param tool_name [String] Name of the tool
|
|
341
|
+
# @param error_info [Hash] Error information from preview
|
|
342
|
+
# @return [String] Feedback message
|
|
343
|
+
private def build_preview_error_feedback(tool_name, error_info)
|
|
344
|
+
case tool_name
|
|
345
|
+
when "edit"
|
|
346
|
+
"Tool edit denied: The edit operation will fail because the old_string was not found in the file. " \
|
|
347
|
+
"Please use file_reader to read '#{error_info[:path]}' first, " \
|
|
348
|
+
"find the correct string to replace, and try again with the exact string (including whitespace)."
|
|
349
|
+
else
|
|
350
|
+
"Tool preview error: #{error_info[:error]}"
|
|
351
|
+
end
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Show preview for write tool
|
|
355
|
+
# @param args [Hash] Write tool arguments
|
|
356
|
+
# @return [nil] Always returns nil (no errors for write)
|
|
357
|
+
private def show_write_preview(args)
|
|
358
|
+
path = args[:path] || args['path']
|
|
359
|
+
# Expand ~ to home directory so File.exist? works correctly
|
|
360
|
+
expanded_path = path&.start_with?("~") ? File.expand_path(path) : path
|
|
361
|
+
new_content = args[:content] || args['content'] || ""
|
|
362
|
+
|
|
363
|
+
is_new_file = !(expanded_path && File.exist?(expanded_path))
|
|
364
|
+
@ui&.show_file_write_preview(path, is_new_file: is_new_file)
|
|
365
|
+
|
|
366
|
+
if is_new_file
|
|
367
|
+
@ui&.show_diff("", new_content, max_lines: 50)
|
|
368
|
+
else
|
|
369
|
+
old_content = File.read(expanded_path)
|
|
370
|
+
old_content = old_content.encode("UTF-8", invalid: :replace, undef: :replace, replace: "\u{FFFD}") unless old_content.encoding == Encoding::UTF_8 && old_content.valid_encoding?
|
|
371
|
+
@ui&.show_diff(old_content, new_content, max_lines: 50)
|
|
372
|
+
end
|
|
373
|
+
nil
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# Show preview for edit tool
|
|
377
|
+
# @param args [Hash] Edit tool arguments
|
|
378
|
+
# @return [Hash, nil] Error information if preview detected issues
|
|
379
|
+
private def show_edit_preview(args)
|
|
380
|
+
path = args[:path] || args[:file_path] || args['path'] || args['file_path']
|
|
381
|
+
old_string = args[:old_string] || args['old_string'] || ""
|
|
382
|
+
new_string = args[:new_string] || args['new_string'] || ""
|
|
383
|
+
replace_all = args[:replace_all] || args['replace_all'] || false
|
|
384
|
+
|
|
385
|
+
# Expand ~ to home directory so File.exist? and File.read work correctly
|
|
386
|
+
expanded_path = path&.start_with?("~") ? File.expand_path(path) : path
|
|
387
|
+
|
|
388
|
+
@ui&.show_file_edit_preview(path)
|
|
389
|
+
|
|
390
|
+
if !expanded_path || expanded_path.empty?
|
|
391
|
+
@ui&.show_file_error("No file path provided")
|
|
392
|
+
return { error: "No file path provided for edit operation" }
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
unless File.exist?(expanded_path)
|
|
396
|
+
@ui&.show_file_error("File not found: #{path}")
|
|
397
|
+
return { error: "File not found: #{path}", path: path }
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
if old_string.empty?
|
|
401
|
+
@ui&.show_file_error("No old_string provided (nothing to replace)")
|
|
402
|
+
return { error: "No old_string provided (nothing to replace)" }
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
file_content = File.read(expanded_path)
|
|
406
|
+
|
|
407
|
+
# Use the same find_match logic as Edit tool to handle fuzzy matching
|
|
408
|
+
# (trim, unescape, smart line matching) — prevents diff from being blank
|
|
409
|
+
# when simple include? fails but Edit#execute's fuzzy match would succeed
|
|
410
|
+
match_result = Utils::StringMatcher.find_match(file_content, old_string)
|
|
411
|
+
|
|
412
|
+
unless match_result
|
|
413
|
+
# Log debug info for troubleshooting
|
|
414
|
+
@debug_logs << {
|
|
415
|
+
timestamp: Time.now.iso8601,
|
|
416
|
+
event: "edit_preview_failed",
|
|
417
|
+
path: path,
|
|
418
|
+
looking_for: old_string[0..500],
|
|
419
|
+
file_content_preview: file_content[0..1000],
|
|
420
|
+
file_size: file_content.length
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
@ui&.show_file_error("Edit file error")
|
|
424
|
+
return {
|
|
425
|
+
error: "String to replace not found in file",
|
|
426
|
+
path: path,
|
|
427
|
+
looking_for: old_string[0..200]
|
|
428
|
+
}
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
# Use the actual matched string (may differ via trim/unescape) for replacement
|
|
432
|
+
actual_old_string = match_result[:matched_string]
|
|
433
|
+
|
|
434
|
+
# Use the same replace logic as the actual tool execution
|
|
435
|
+
new_content = if replace_all
|
|
436
|
+
file_content.gsub(actual_old_string, new_string)
|
|
437
|
+
else
|
|
438
|
+
file_content.sub(actual_old_string, new_string)
|
|
439
|
+
end
|
|
440
|
+
@ui&.show_diff(file_content, new_content, max_lines: 50)
|
|
441
|
+
nil # No error
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# Show preview for shell tool
|
|
445
|
+
# @param args [Hash] Shell tool arguments
|
|
446
|
+
# @return [nil] Always returns nil
|
|
447
|
+
private def show_shell_preview(args)
|
|
448
|
+
command = args[:command] || ""
|
|
449
|
+
@ui&.show_shell_preview(command)
|
|
450
|
+
nil
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
end
|
|
454
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Octo
|
|
4
|
+
class ToolRegistry
|
|
5
|
+
# Common aliases that LLMs frequently use instead of the registered tool names.
|
|
6
|
+
# Keys are downcased aliases; values are the canonical registered names.
|
|
7
|
+
TOOL_ALIASES = {
|
|
8
|
+
# file_reader aliases
|
|
9
|
+
"read" => "file_reader",
|
|
10
|
+
"read_file" => "file_reader",
|
|
11
|
+
"filereader" => "file_reader",
|
|
12
|
+
"file_read" => "file_reader",
|
|
13
|
+
"cat" => "file_reader",
|
|
14
|
+
# write aliases
|
|
15
|
+
"write_file" => "write",
|
|
16
|
+
"create_file" => "write",
|
|
17
|
+
"file_write" => "write",
|
|
18
|
+
# edit aliases
|
|
19
|
+
"file_edit" => "edit",
|
|
20
|
+
"replace" => "edit",
|
|
21
|
+
"replace_in_file" => "edit",
|
|
22
|
+
"str_replace" => "edit",
|
|
23
|
+
# terminal aliases
|
|
24
|
+
"shell" => "terminal",
|
|
25
|
+
"bash" => "terminal",
|
|
26
|
+
"exec" => "terminal",
|
|
27
|
+
"execute" => "terminal",
|
|
28
|
+
"run_command" => "terminal",
|
|
29
|
+
"run" => "terminal",
|
|
30
|
+
"command" => "terminal",
|
|
31
|
+
# web_search aliases
|
|
32
|
+
"search" => "web_search",
|
|
33
|
+
"websearch" => "web_search",
|
|
34
|
+
"internet_search" => "web_search",
|
|
35
|
+
"online_search" => "web_search",
|
|
36
|
+
# web_fetch aliases
|
|
37
|
+
"fetch" => "web_fetch",
|
|
38
|
+
"webfetch" => "web_fetch",
|
|
39
|
+
"browse" => "web_fetch",
|
|
40
|
+
"url_fetch" => "web_fetch",
|
|
41
|
+
"http_get" => "web_fetch",
|
|
42
|
+
# grep aliases
|
|
43
|
+
"search_files" => "grep",
|
|
44
|
+
"search_in_files" => "grep",
|
|
45
|
+
"find_in_files" => "grep",
|
|
46
|
+
"search_code" => "grep",
|
|
47
|
+
# glob aliases
|
|
48
|
+
"find_files" => "glob",
|
|
49
|
+
"list_files" => "glob",
|
|
50
|
+
"file_glob" => "glob",
|
|
51
|
+
"search_filenames" => "glob",
|
|
52
|
+
# invoke_skill aliases
|
|
53
|
+
"skill" => "invoke_skill",
|
|
54
|
+
"run_skill" => "invoke_skill",
|
|
55
|
+
# todo_manager aliases
|
|
56
|
+
"todo" => "todo_manager",
|
|
57
|
+
"task_manager" => "todo_manager",
|
|
58
|
+
# request_user_feedback aliases
|
|
59
|
+
"ask_user" => "request_user_feedback",
|
|
60
|
+
"user_feedback" => "request_user_feedback",
|
|
61
|
+
"ask" => "request_user_feedback",
|
|
62
|
+
# undo_task aliases
|
|
63
|
+
"undo" => "undo_task",
|
|
64
|
+
# redo_task aliases
|
|
65
|
+
"redo" => "redo_task",
|
|
66
|
+
# list_tasks aliases
|
|
67
|
+
"tasks" => "list_tasks",
|
|
68
|
+
"task_history" => "list_tasks",
|
|
69
|
+
# trash_manager aliases
|
|
70
|
+
"trash" => "trash_manager",
|
|
71
|
+
"delete" => "trash_manager",
|
|
72
|
+
"rm" => "trash_manager",
|
|
73
|
+
"remove" => "trash_manager",
|
|
74
|
+
}.freeze
|
|
75
|
+
|
|
76
|
+
def initialize
|
|
77
|
+
@tools = {}
|
|
78
|
+
# Downcased index for case-insensitive lookups
|
|
79
|
+
@downcased_index = {}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def register(tool)
|
|
83
|
+
@tools[tool.name] = tool
|
|
84
|
+
@downcased_index[tool.name.downcase] = tool.name
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def get(name)
|
|
88
|
+
@tools[name] || raise(Octo::ToolCallError, "Tool not found: #{name}")
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Resolve a tool name (possibly misspelt or aliased) to the canonical
|
|
92
|
+
# registered name. Resolution order:
|
|
93
|
+
# 1. Exact match in the registry
|
|
94
|
+
# 2. Case-insensitive match (e.g. "Read" → "file_reader")
|
|
95
|
+
# 3. Alias lookup (e.g. "read_file" → "file_reader")
|
|
96
|
+
# Returns the canonical tool name, or nil if nothing matched.
|
|
97
|
+
def resolve(name)
|
|
98
|
+
return name if @tools.key?(name)
|
|
99
|
+
|
|
100
|
+
downcased = name.downcase
|
|
101
|
+
|
|
102
|
+
# Case-insensitive match
|
|
103
|
+
if @downcased_index.key?(downcased)
|
|
104
|
+
return @downcased_index[downcased]
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Alias lookup
|
|
108
|
+
if TOOL_ALIASES.key?(downcased)
|
|
109
|
+
return TOOL_ALIASES[downcased]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Fuzzy: try underscore / hyphen normalisation (e.g. "file-reader" → "file_reader")
|
|
113
|
+
normalized = downcased.tr("-", "_")
|
|
114
|
+
if normalized != downcased
|
|
115
|
+
if @downcased_index.key?(normalized)
|
|
116
|
+
return @downcased_index[normalized]
|
|
117
|
+
end
|
|
118
|
+
if TOOL_ALIASES.key?(normalized)
|
|
119
|
+
return TOOL_ALIASES[normalized]
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
nil
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def all
|
|
127
|
+
@tools.values
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def all_definitions
|
|
131
|
+
@tools.values.map(&:to_function_definition)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def allowed_definitions(allowed_tools = nil)
|
|
135
|
+
return all_definitions if allowed_tools.nil? || allowed_tools.include?("all")
|
|
136
|
+
|
|
137
|
+
@tools.select { |name, _| allowed_tools.include?(name) }
|
|
138
|
+
.values
|
|
139
|
+
.map(&:to_function_definition)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def tool_names
|
|
143
|
+
@tools.keys
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def by_category(category)
|
|
147
|
+
@tools.values.select { |tool| tool.category == category }
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|