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,162 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
module Octo
|
|
6
|
+
module Tools
|
|
7
|
+
class Glob < Base
|
|
8
|
+
# Maximum file size to search (1MB)
|
|
9
|
+
MAX_FILE_SIZE = 1_048_576
|
|
10
|
+
|
|
11
|
+
self.tool_name = "glob"
|
|
12
|
+
self.tool_description = "Find files matching a glob pattern (e.g., '**/*.rb', 'src/**/*.js'). " \
|
|
13
|
+
"Returns file paths sorted by modification time. Respects .gitignore patterns."
|
|
14
|
+
self.tool_category = "file_system"
|
|
15
|
+
self.tool_parameters = {
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: {
|
|
18
|
+
pattern: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "The glob pattern to match files (e.g., '**/*.rb', 'lib/**/*.rb', '*.txt')"
|
|
21
|
+
},
|
|
22
|
+
base_path: {
|
|
23
|
+
type: "string",
|
|
24
|
+
description: "The base directory to search in (defaults to current directory)",
|
|
25
|
+
default: "."
|
|
26
|
+
},
|
|
27
|
+
limit: {
|
|
28
|
+
type: "integer",
|
|
29
|
+
description: "Maximum number of results to return (default: 10)",
|
|
30
|
+
default: 10
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
required: %w[pattern]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
def execute(pattern:, base_path: ".", limit: 10, working_dir: nil)
|
|
37
|
+
# Validate pattern
|
|
38
|
+
if pattern.nil? || pattern.strip.empty?
|
|
39
|
+
return { error: "Pattern cannot be empty" }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Expand ~ in pattern only (pattern is relative to base_path, not working_dir)
|
|
43
|
+
pattern = pattern.start_with?("~") ? File.expand_path(pattern) : pattern
|
|
44
|
+
# Expand base_path fully (~ and relative paths resolved against working_dir)
|
|
45
|
+
base_path = expand_path(base_path, working_dir: working_dir)
|
|
46
|
+
|
|
47
|
+
# Validate base_path
|
|
48
|
+
unless Dir.exist?(base_path)
|
|
49
|
+
return { error: "Base path does not exist: #{base_path}" }
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
begin
|
|
53
|
+
expanded_path = base_path
|
|
54
|
+
|
|
55
|
+
skipped = {
|
|
56
|
+
binary: 0,
|
|
57
|
+
too_large: 0,
|
|
58
|
+
ignored: 0
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
# Auto-expand bare patterns (no slash, no **) to recursive search.
|
|
62
|
+
effective_pattern = if !File.absolute_path?(pattern) &&
|
|
63
|
+
!pattern.include?("/") &&
|
|
64
|
+
!pattern.start_with?("**")
|
|
65
|
+
"**/#{pattern}"
|
|
66
|
+
else
|
|
67
|
+
pattern
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
fnmatch_flags = File::FNM_PATHNAME | File::FNM_DOTMATCH
|
|
71
|
+
|
|
72
|
+
matches = []
|
|
73
|
+
Octo::Utils::FileIgnoreHelper.walk_files(expanded_path, skipped: skipped) do |file|
|
|
74
|
+
relative = file[(expanded_path.length + 1)..]
|
|
75
|
+
|
|
76
|
+
unless File.fnmatch(effective_pattern, relative, fnmatch_flags)
|
|
77
|
+
next
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
if Octo::Utils::FileProcessor.binary_file_path?(file) &&
|
|
81
|
+
!Octo::Utils::FileProcessor.glob_allowed_binary?(file)
|
|
82
|
+
skipped[:binary] += 1
|
|
83
|
+
next
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
if File.size(file) > MAX_FILE_SIZE
|
|
87
|
+
skipped[:too_large] += 1
|
|
88
|
+
next
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
matches << file
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Sort by modification time (most recent first)
|
|
95
|
+
matches = matches.sort_by { |path| -File.mtime(path).to_i }
|
|
96
|
+
|
|
97
|
+
# Apply limit
|
|
98
|
+
total_matches = matches.length
|
|
99
|
+
matches = matches.take(limit)
|
|
100
|
+
|
|
101
|
+
# Convert to absolute paths
|
|
102
|
+
matches = matches.map { |path| File.expand_path(path) }
|
|
103
|
+
|
|
104
|
+
{
|
|
105
|
+
matches: matches,
|
|
106
|
+
total_matches: total_matches,
|
|
107
|
+
returned: matches.length,
|
|
108
|
+
truncated: total_matches > limit,
|
|
109
|
+
skipped_files: skipped,
|
|
110
|
+
error: nil
|
|
111
|
+
}
|
|
112
|
+
rescue StandardError => e
|
|
113
|
+
{ error: "Failed to glob files: #{e.message}" }
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def format_call(args)
|
|
118
|
+
pattern = args[:pattern] || args['pattern'] || ''
|
|
119
|
+
base_path = args[:base_path] || args['base_path'] || '.'
|
|
120
|
+
|
|
121
|
+
display_base = base_path == '.' ? '' : " in #{base_path}"
|
|
122
|
+
"glob(\"#{pattern}\"#{display_base})"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def format_result(result)
|
|
126
|
+
if result[:error]
|
|
127
|
+
"[Error] #{result[:error]}"
|
|
128
|
+
else
|
|
129
|
+
count = result[:returned] || 0
|
|
130
|
+
total = result[:total_matches] || 0
|
|
131
|
+
truncated = result[:truncated] ? " (truncated)" : ""
|
|
132
|
+
|
|
133
|
+
msg = "[OK] Found #{count}/#{total} files#{truncated}"
|
|
134
|
+
|
|
135
|
+
# Add skipped files info if present
|
|
136
|
+
if result[:skipped_files]
|
|
137
|
+
skipped = result[:skipped_files]
|
|
138
|
+
skipped_parts = []
|
|
139
|
+
skipped_parts << "#{skipped[:ignored]} ignored" if skipped[:ignored] > 0
|
|
140
|
+
skipped_parts << "#{skipped[:binary]} binary" if skipped[:binary] > 0
|
|
141
|
+
skipped_parts << "#{skipped[:too_large]} too large" if skipped[:too_large] > 0
|
|
142
|
+
|
|
143
|
+
msg += " (skipped: #{skipped_parts.join(', ')})" unless skipped_parts.empty?
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
msg
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def format_result_for_ui(result)
|
|
151
|
+
return nil if result[:error]
|
|
152
|
+
{
|
|
153
|
+
type: "file_list",
|
|
154
|
+
path: result[:base_path] || ".",
|
|
155
|
+
entries: (result[:matches] || []).map { |p| { name: File.basename(p), is_dir: false } },
|
|
156
|
+
total: result[:total_matches] || 0,
|
|
157
|
+
truncated: result[:truncated] || false
|
|
158
|
+
}
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Octo
|
|
4
|
+
module Tools
|
|
5
|
+
class Grep < Base
|
|
6
|
+
# Maximum file size to search (1MB)
|
|
7
|
+
MAX_FILE_SIZE = 1_048_576
|
|
8
|
+
|
|
9
|
+
# Maximum line length to display (to avoid huge outputs)
|
|
10
|
+
MAX_LINE_LENGTH = 500
|
|
11
|
+
|
|
12
|
+
self.tool_name = "grep"
|
|
13
|
+
self.tool_description = "Search file contents using regular expressions. Returns matching lines with context."
|
|
14
|
+
self.tool_category = "file_system"
|
|
15
|
+
self.tool_parameters = {
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: {
|
|
18
|
+
pattern: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "The regular expression pattern to search for"
|
|
21
|
+
},
|
|
22
|
+
path: {
|
|
23
|
+
type: "string",
|
|
24
|
+
description: "File or directory to search in (defaults to current directory)",
|
|
25
|
+
default: "."
|
|
26
|
+
},
|
|
27
|
+
file_pattern: {
|
|
28
|
+
type: "string",
|
|
29
|
+
description: "Glob pattern to filter files (e.g., '*.rb', '**/*.js')",
|
|
30
|
+
default: "**/*"
|
|
31
|
+
},
|
|
32
|
+
case_insensitive: {
|
|
33
|
+
type: "boolean",
|
|
34
|
+
description: "Perform case-insensitive search",
|
|
35
|
+
default: false
|
|
36
|
+
},
|
|
37
|
+
context_lines: {
|
|
38
|
+
type: "integer",
|
|
39
|
+
description: "Number of context lines to show before and after each match (max: 10)",
|
|
40
|
+
default: 0
|
|
41
|
+
},
|
|
42
|
+
max_files: {
|
|
43
|
+
type: "integer",
|
|
44
|
+
description: "Maximum number of matching files to return",
|
|
45
|
+
default: 50
|
|
46
|
+
},
|
|
47
|
+
max_matches_per_file: {
|
|
48
|
+
type: "integer",
|
|
49
|
+
description: "Maximum number of matches to return per file",
|
|
50
|
+
default: 50
|
|
51
|
+
},
|
|
52
|
+
max_total_matches: {
|
|
53
|
+
type: "integer",
|
|
54
|
+
description: "Maximum total number of matches to return across all files",
|
|
55
|
+
default: 200
|
|
56
|
+
},
|
|
57
|
+
max_file_size: {
|
|
58
|
+
type: "integer",
|
|
59
|
+
description: "Maximum file size in bytes to search (default: 1MB)",
|
|
60
|
+
default: MAX_FILE_SIZE
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
required: %w[pattern]
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
def execute(
|
|
67
|
+
pattern:,
|
|
68
|
+
path: ".",
|
|
69
|
+
file_pattern: "**/*",
|
|
70
|
+
case_insensitive: false,
|
|
71
|
+
context_lines: 0,
|
|
72
|
+
max_files: 50,
|
|
73
|
+
max_matches_per_file: 50,
|
|
74
|
+
max_total_matches: 200,
|
|
75
|
+
max_file_size: MAX_FILE_SIZE,
|
|
76
|
+
max_files_to_search: 10000,
|
|
77
|
+
working_dir: nil
|
|
78
|
+
)
|
|
79
|
+
# Validate pattern
|
|
80
|
+
if pattern.nil? || pattern.strip.empty?
|
|
81
|
+
return { error: "Pattern cannot be empty" }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Validate and expand path relative to working_dir when provided
|
|
85
|
+
begin
|
|
86
|
+
expanded_path = expand_path(path, working_dir: working_dir)
|
|
87
|
+
rescue StandardError => e
|
|
88
|
+
return { error: "Invalid path: #{e.message}" }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
unless File.exist?(expanded_path)
|
|
92
|
+
return { error: "Path does not exist: #{path}" }
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Limit context_lines
|
|
96
|
+
context_lines = [[context_lines, 0].max, 10].min
|
|
97
|
+
|
|
98
|
+
begin
|
|
99
|
+
# Compile regex
|
|
100
|
+
regex_options = case_insensitive ? Regexp::IGNORECASE : 0
|
|
101
|
+
regex = Regexp.new(pattern, regex_options)
|
|
102
|
+
|
|
103
|
+
results = []
|
|
104
|
+
total_matches = 0
|
|
105
|
+
files_searched = 0
|
|
106
|
+
skipped = {
|
|
107
|
+
binary: 0,
|
|
108
|
+
too_large: 0,
|
|
109
|
+
ignored: 0
|
|
110
|
+
}
|
|
111
|
+
truncation_reason = nil
|
|
112
|
+
|
|
113
|
+
files = if File.file?(expanded_path)
|
|
114
|
+
[expanded_path]
|
|
115
|
+
else
|
|
116
|
+
fnmatch_flags = File::FNM_PATHNAME | File::FNM_DOTMATCH
|
|
117
|
+
collected = []
|
|
118
|
+
Octo::Utils::FileIgnoreHelper.walk_files(expanded_path, skipped: skipped) do |f|
|
|
119
|
+
relative = f[(expanded_path.length + 1)..]
|
|
120
|
+
collected << f if File.fnmatch(file_pattern, relative, fnmatch_flags)
|
|
121
|
+
end
|
|
122
|
+
collected
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
files.each do |file|
|
|
126
|
+
if files_searched >= max_files_to_search
|
|
127
|
+
truncation_reason ||= "max_files_to_search limit reached"
|
|
128
|
+
break
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# Skip binary files
|
|
132
|
+
if Octo::Utils::FileProcessor.binary_file_path?(file)
|
|
133
|
+
skipped[:binary] += 1
|
|
134
|
+
next
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# Skip files that are too large
|
|
138
|
+
if File.size(file) > max_file_size
|
|
139
|
+
skipped[:too_large] += 1
|
|
140
|
+
next
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
files_searched += 1
|
|
144
|
+
|
|
145
|
+
# Check if we've found enough matching files
|
|
146
|
+
if results.length >= max_files
|
|
147
|
+
truncation_reason ||= "max_files limit reached"
|
|
148
|
+
break
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Check if we've found enough total matches
|
|
152
|
+
if total_matches >= max_total_matches
|
|
153
|
+
truncation_reason ||= "max_total_matches limit reached"
|
|
154
|
+
break
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
# Search the file
|
|
158
|
+
matches = search_file(file, regex, context_lines, max_matches_per_file)
|
|
159
|
+
next if matches.empty?
|
|
160
|
+
|
|
161
|
+
# Add remaining matches respecting max_total_matches
|
|
162
|
+
remaining_matches = max_total_matches - total_matches
|
|
163
|
+
matches = matches.take(remaining_matches) if remaining_matches < matches.length
|
|
164
|
+
|
|
165
|
+
results << {
|
|
166
|
+
file: File.expand_path(file),
|
|
167
|
+
matches: matches
|
|
168
|
+
}
|
|
169
|
+
total_matches += matches.length
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
{
|
|
173
|
+
results: results,
|
|
174
|
+
total_matches: total_matches,
|
|
175
|
+
files_searched: files_searched,
|
|
176
|
+
files_with_matches: results.length,
|
|
177
|
+
skipped_files: skipped,
|
|
178
|
+
truncated: !truncation_reason.nil?,
|
|
179
|
+
truncation_reason: truncation_reason,
|
|
180
|
+
error: nil
|
|
181
|
+
}
|
|
182
|
+
rescue RegexpError => e
|
|
183
|
+
{ error: "Invalid regex pattern: #{e.message}" }
|
|
184
|
+
rescue StandardError => e
|
|
185
|
+
{ error: "Failed to search files: #{e.message}" }
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def format_call(args)
|
|
190
|
+
pattern = args[:pattern] || args['pattern'] || ''
|
|
191
|
+
path = args[:path] || args['path'] || '.'
|
|
192
|
+
|
|
193
|
+
# Truncate pattern if too long
|
|
194
|
+
display_pattern = pattern.length > 30 ? "#{pattern[0..27]}..." : pattern
|
|
195
|
+
display_path = path == '.' ? 'current dir' : (path.length > 20 ? "...#{path[-17..]}" : path)
|
|
196
|
+
|
|
197
|
+
"grep(\"#{display_pattern}\" in #{display_path})"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def format_result(result)
|
|
201
|
+
if result[:error]
|
|
202
|
+
"[Error] #{result[:error]}"
|
|
203
|
+
else
|
|
204
|
+
matches = result[:total_matches] || 0
|
|
205
|
+
files = result[:files_with_matches] || 0
|
|
206
|
+
msg = "[OK] Found #{matches} matches in #{files} files"
|
|
207
|
+
|
|
208
|
+
# Add truncation info if present
|
|
209
|
+
if result[:truncated] && result[:truncation_reason]
|
|
210
|
+
msg += " (truncated: #{result[:truncation_reason]})"
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
msg
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def format_result_for_ui(result)
|
|
218
|
+
return nil if result[:error]
|
|
219
|
+
|
|
220
|
+
matches = result[:results]&.flat_map do |r|
|
|
221
|
+
r[:matches]&.map do |m|
|
|
222
|
+
{
|
|
223
|
+
file: r[:file],
|
|
224
|
+
line_no: m[:line_number],
|
|
225
|
+
line: m[:line],
|
|
226
|
+
context: m[:context]
|
|
227
|
+
}
|
|
228
|
+
end
|
|
229
|
+
end&.compact || []
|
|
230
|
+
|
|
231
|
+
{
|
|
232
|
+
type: "search",
|
|
233
|
+
pattern: result[:pattern],
|
|
234
|
+
path: result[:path],
|
|
235
|
+
matches: matches.first(20),
|
|
236
|
+
total_matches: result[:total_matches],
|
|
237
|
+
files_with_matches: result[:files_with_matches],
|
|
238
|
+
truncated: result[:truncated],
|
|
239
|
+
truncation_reason: result[:truncation_reason]
|
|
240
|
+
}
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# Format result for LLM consumption - return a compact version to save tokens
|
|
244
|
+
def format_result_for_llm(result)
|
|
245
|
+
# If there's an error, return it as-is
|
|
246
|
+
return result if result[:error]
|
|
247
|
+
|
|
248
|
+
# Build a compact summary with file list and sample matches
|
|
249
|
+
compact = {
|
|
250
|
+
summary: {
|
|
251
|
+
total_matches: result[:total_matches],
|
|
252
|
+
files_with_matches: result[:files_with_matches],
|
|
253
|
+
files_searched: result[:files_searched],
|
|
254
|
+
truncated: result[:truncated],
|
|
255
|
+
truncation_reason: result[:truncation_reason]
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
# Include list of files with match counts
|
|
260
|
+
if result[:results] && !result[:results].empty?
|
|
261
|
+
compact[:files] = result[:results].map do |file_result|
|
|
262
|
+
{
|
|
263
|
+
file: file_result[:file],
|
|
264
|
+
match_count: file_result[:matches].length
|
|
265
|
+
}
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Include sample matches (first 2 matches from first 3 files) for context
|
|
269
|
+
sample_results = result[:results].take(3)
|
|
270
|
+
compact[:sample_matches] = sample_results.map do |file_result|
|
|
271
|
+
{
|
|
272
|
+
file: file_result[:file],
|
|
273
|
+
matches: file_result[:matches].take(2).map do |match|
|
|
274
|
+
{
|
|
275
|
+
line_number: match[:line_number],
|
|
276
|
+
line: match[:line]
|
|
277
|
+
# Omit context to save space - it's rarely needed by LLM
|
|
278
|
+
}
|
|
279
|
+
end
|
|
280
|
+
}
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
compact
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def search_file(file, regex, context_lines, max_matches)
|
|
289
|
+
matches = []
|
|
290
|
+
|
|
291
|
+
# Use File.foreach for memory-efficient line-by-line reading.
|
|
292
|
+
# Scrub invalid UTF-8 bytes so results survive JSON encoding.
|
|
293
|
+
File.foreach(file, chomp: true).with_index do |raw_line, index|
|
|
294
|
+
line = safe_utf8(raw_line)
|
|
295
|
+
# Stop if we have enough matches for this file
|
|
296
|
+
break if matches.length >= max_matches
|
|
297
|
+
|
|
298
|
+
next unless line.match?(regex)
|
|
299
|
+
|
|
300
|
+
# Truncate long lines
|
|
301
|
+
display_line = line.length > MAX_LINE_LENGTH ? "#{line[0...MAX_LINE_LENGTH]}..." : line
|
|
302
|
+
|
|
303
|
+
# Get context if requested
|
|
304
|
+
if context_lines > 0
|
|
305
|
+
context = get_line_context(file, index, context_lines)
|
|
306
|
+
else
|
|
307
|
+
context = nil
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
matches << {
|
|
311
|
+
line_number: index + 1,
|
|
312
|
+
line: display_line,
|
|
313
|
+
context: context
|
|
314
|
+
}
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
matches
|
|
318
|
+
rescue StandardError
|
|
319
|
+
[]
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Get context lines around a match
|
|
323
|
+
def get_line_context(file, match_index, context_lines)
|
|
324
|
+
lines = File.readlines(file, chomp: true).map! { |l| safe_utf8(l) }
|
|
325
|
+
start_line = [0, match_index - context_lines].max
|
|
326
|
+
end_line = [lines.length - 1, match_index + context_lines].min
|
|
327
|
+
|
|
328
|
+
context = []
|
|
329
|
+
(start_line..end_line).each do |i|
|
|
330
|
+
line_content = lines[i]
|
|
331
|
+
# Truncate long lines in context too
|
|
332
|
+
display_content = line_content.length > MAX_LINE_LENGTH ?
|
|
333
|
+
"#{line_content[0...MAX_LINE_LENGTH]}..." :
|
|
334
|
+
line_content
|
|
335
|
+
|
|
336
|
+
context << {
|
|
337
|
+
line_number: i + 1,
|
|
338
|
+
content: display_content,
|
|
339
|
+
is_match: i == match_index
|
|
340
|
+
}
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
context
|
|
344
|
+
rescue StandardError
|
|
345
|
+
nil
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# Scrub invalid UTF-8 byte sequences (see file_reader.rb for rationale).
|
|
349
|
+
private def safe_utf8(str)
|
|
350
|
+
return str if str.nil?
|
|
351
|
+
return str if str.encoding == Encoding::UTF_8 && str.valid_encoding?
|
|
352
|
+
str.encode("UTF-8", invalid: :replace, undef: :replace, replace: "\u{FFFD}")
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
end
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Octo
|
|
4
|
+
module Tools
|
|
5
|
+
# Tool for invoking skills within the agent
|
|
6
|
+
# This allows the AI to call skills as tools rather than requiring explicit user commands
|
|
7
|
+
class InvokeSkill < Base
|
|
8
|
+
self.tool_name = "invoke_skill"
|
|
9
|
+
self.tool_description = "Invoke a specialized skill to handle specific tasks. Use this when user's request matches a skill's description (e.g., code exploration, document creation, etc.). This will read the skill's instructions and execute them appropriately (either inline or in a subagent)."
|
|
10
|
+
self.tool_category = "skill_management"
|
|
11
|
+
self.tool_parameters = {
|
|
12
|
+
type: "object",
|
|
13
|
+
properties: {
|
|
14
|
+
skill_name: {
|
|
15
|
+
type: "string",
|
|
16
|
+
description: "Name of the skill to invoke (e.g., 'code-explorer', 'pptx', 'pdf')"
|
|
17
|
+
},
|
|
18
|
+
task: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "The task or query to pass to the skill"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
required: ["skill_name", "task"]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
# Execute the skill invocation
|
|
27
|
+
# @param skill_name [String] Name of the skill to invoke
|
|
28
|
+
# @param task [String] Task description to pass to the skill
|
|
29
|
+
# @param agent [Octo::Agent] Agent instance (injected)
|
|
30
|
+
# @param skill_loader [Octo::SkillLoader] Skill loader instance (injected)
|
|
31
|
+
# @return [Hash] Result of skill execution
|
|
32
|
+
def execute(skill_name:, task:, agent: nil, skill_loader: nil, working_dir: nil)
|
|
33
|
+
# Validate injected dependencies
|
|
34
|
+
return { error: "Agent context is required" } unless agent
|
|
35
|
+
return { error: "Skill loader is required" } unless skill_loader
|
|
36
|
+
|
|
37
|
+
# Find skill by name
|
|
38
|
+
skill = skill_loader.find_by_name(skill_name)
|
|
39
|
+
return { error: "Skill not found: #{skill_name}" } unless skill
|
|
40
|
+
|
|
41
|
+
# Execute skill based on its configuration.
|
|
42
|
+
# Note: disable-model-invocation only prevents the skill from appearing in AVAILABLE SKILLS
|
|
43
|
+
# (so the model won't auto-discover it). It does NOT block execution here — the user may
|
|
44
|
+
# have triggered this skill explicitly via a slash command (/skill-name).
|
|
45
|
+
if skill.fork_agent?
|
|
46
|
+
# Execute in isolated subagent
|
|
47
|
+
result = agent.send(:execute_skill_with_subagent, skill, task)
|
|
48
|
+
{
|
|
49
|
+
message: "Skill '#{skill_name}' executed in subagent",
|
|
50
|
+
result: result,
|
|
51
|
+
skill_type: "subagent"
|
|
52
|
+
}
|
|
53
|
+
else
|
|
54
|
+
# Deferred injection path: enqueue the skill inject on the agent.
|
|
55
|
+
#
|
|
56
|
+
# Injecting inside execute() would produce an illegal message ordering for Bedrock:
|
|
57
|
+
# assistant: {toolUse: invoke_skill}
|
|
58
|
+
# assistant: {text: skill_instructions} ← injected here (breaks pairing)
|
|
59
|
+
# user: {toolResult: invoke_skill} ← observe() appends this too late
|
|
60
|
+
#
|
|
61
|
+
# Instead, enqueue the injection so the agent loop can flush it AFTER observe()
|
|
62
|
+
# appends the toolResult, producing the correct sequence:
|
|
63
|
+
# assistant: {toolUse: invoke_skill}
|
|
64
|
+
# user: {toolResult: ...} ← observe() appends first
|
|
65
|
+
# assistant: {text: skill_instructions} ← flush_pending_injections runs here
|
|
66
|
+
# user: "[SYSTEM] please proceed"
|
|
67
|
+
agent.enqueue_injection(skill, task)
|
|
68
|
+
"Skill '#{skill_name}' instructions expanded. Proceed to execute the task."
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Format the tool call for display
|
|
73
|
+
# @param args [Hash] Tool arguments
|
|
74
|
+
# @return [String] Formatted call description
|
|
75
|
+
def format_call(args)
|
|
76
|
+
skill = args[:skill_name] || args["skill_name"]
|
|
77
|
+
"InvokeSkill(#{skill})"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Format the tool result for display
|
|
81
|
+
# @param result [Hash] Tool execution result
|
|
82
|
+
# @return [String] Formatted result summary
|
|
83
|
+
def format_result(result)
|
|
84
|
+
if result.is_a?(String)
|
|
85
|
+
result
|
|
86
|
+
elsif result[:error]
|
|
87
|
+
"Error: #{result[:error]}"
|
|
88
|
+
elsif result[:skill_type] == "subagent"
|
|
89
|
+
"Subagent executed successfully"
|
|
90
|
+
else
|
|
91
|
+
"Skill content expanded"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Octo
|
|
4
|
+
module Tools
|
|
5
|
+
# Tool for listing task history (Time Machine feature)
|
|
6
|
+
class ListTasks < Base
|
|
7
|
+
self.tool_name = "list_tasks"
|
|
8
|
+
self.tool_description = "List recent tasks in the task history with summaries. " \
|
|
9
|
+
"Shows current task, past tasks, and future tasks (after undo). " \
|
|
10
|
+
"Use when user wants to see task history or choose which task to undo/redo to."
|
|
11
|
+
self.tool_category = "time_machine"
|
|
12
|
+
self.tool_parameters = {
|
|
13
|
+
type: "object",
|
|
14
|
+
properties: {
|
|
15
|
+
limit: {
|
|
16
|
+
type: "integer",
|
|
17
|
+
description: "Maximum number of recent tasks to show (default: 10)",
|
|
18
|
+
default: 10
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
def execute(agent:, limit: 10, **_args)
|
|
24
|
+
history = agent.get_task_history(limit: limit)
|
|
25
|
+
|
|
26
|
+
if history.empty?
|
|
27
|
+
return "No task history available."
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
lines = ["Task History:"]
|
|
31
|
+
history.each do |task|
|
|
32
|
+
indicator = case task[:status]
|
|
33
|
+
when :current then "→"
|
|
34
|
+
when :past then " "
|
|
35
|
+
when :future then "↯"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
branch_indicator = task[:has_branches] ? " ⎇" : ""
|
|
39
|
+
lines << "#{indicator}#{branch_indicator} Task #{task[:task_id]}: #{task[:summary]}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
lines.join("\n")
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def format_call(limit: 10, **_args)
|
|
46
|
+
"Listing task history (limit: #{limit})..."
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def format_result(result)
|
|
50
|
+
result
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|