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,89 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Octo
|
|
4
|
+
class Agent
|
|
5
|
+
# Scenario 2: Reflect on skill execution and suggest improvements.
|
|
6
|
+
#
|
|
7
|
+
# After a skill completes, forks a subagent to analyze:
|
|
8
|
+
# - Were instructions clear enough?
|
|
9
|
+
# - Any missing edge cases?
|
|
10
|
+
# - Any improvements needed?
|
|
11
|
+
#
|
|
12
|
+
# If the LLM identifies concrete improvements, it invokes skill-creator
|
|
13
|
+
# to update the skill.
|
|
14
|
+
module SkillReflector
|
|
15
|
+
# Minimum iterations for a skill execution to warrant reflection.
|
|
16
|
+
# This counts iterations within the skill execution only, not session-cumulative.
|
|
17
|
+
MIN_SKILL_ITERATIONS = 5
|
|
18
|
+
|
|
19
|
+
# Check if we should reflect on the skill that just executed
|
|
20
|
+
# Called from SkillEvolution#run_skill_evolution_hooks
|
|
21
|
+
def maybe_reflect_on_skill
|
|
22
|
+
return unless @skill_execution_context
|
|
23
|
+
|
|
24
|
+
# Only reflect on skills that the user explicitly invoked via slash command.
|
|
25
|
+
# Skills triggered by the LLM itself (e.g. as part of a broader task) or
|
|
26
|
+
# platform-management skills invoked incidentally should not be reflected on.
|
|
27
|
+
return unless @skill_execution_context[:slash_command]
|
|
28
|
+
|
|
29
|
+
# Skip default skills — they are system-owned and should not be
|
|
30
|
+
# auto-improved by the evolution system.
|
|
31
|
+
source = @skill_execution_context[:source]
|
|
32
|
+
return if source == :default
|
|
33
|
+
|
|
34
|
+
skill_name = @skill_execution_context[:skill_name]
|
|
35
|
+
start_iteration = @skill_execution_context[:start_iteration]
|
|
36
|
+
|
|
37
|
+
# Calculate iterations within the skill execution (not session-cumulative)
|
|
38
|
+
iterations = @iterations - start_iteration
|
|
39
|
+
|
|
40
|
+
# Only reflect if the skill actually ran for a meaningful number of iterations
|
|
41
|
+
return if iterations < MIN_SKILL_ITERATIONS
|
|
42
|
+
|
|
43
|
+
# Fork an isolated subagent to reflect + improve — does NOT touch main history
|
|
44
|
+
@ui&.show_info("Reflecting on skill execution: #{skill_name}")
|
|
45
|
+
subagent = fork_subagent
|
|
46
|
+
result = subagent.run(build_skill_reflection_prompt(skill_name))
|
|
47
|
+
|
|
48
|
+
# Clear the context so we don't reflect again
|
|
49
|
+
@skill_execution_context = nil
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Build the reflection prompt content
|
|
53
|
+
# @param skill_name [String]
|
|
54
|
+
# @return [String]
|
|
55
|
+
private def build_skill_reflection_prompt(skill_name)
|
|
56
|
+
<<~PROMPT
|
|
57
|
+
═══════════════════════════════════════════════════════════════
|
|
58
|
+
SKILL REFLECTION MODE
|
|
59
|
+
═══════════════════════════════════════════════════════════════
|
|
60
|
+
You just executed the skill "#{skill_name}".
|
|
61
|
+
|
|
62
|
+
## Quick Analysis
|
|
63
|
+
|
|
64
|
+
Reflect on whether the skill could be improved:
|
|
65
|
+
- Were the instructions clear enough?
|
|
66
|
+
- Did you encounter any edge cases not covered?
|
|
67
|
+
- Were there any steps that could be streamlined?
|
|
68
|
+
- Is there missing context that would make it easier next time?
|
|
69
|
+
- Did the skill produce the expected results?
|
|
70
|
+
|
|
71
|
+
## Decision
|
|
72
|
+
|
|
73
|
+
If you identified **concrete, actionable improvements**:
|
|
74
|
+
→ Call invoke_skill("skill-creator", task: "Improve skill #{skill_name}: [describe specific improvements needed]")
|
|
75
|
+
|
|
76
|
+
If the skill worked well as-is:
|
|
77
|
+
→ Respond briefly: "Skill #{skill_name} worked well, no improvements needed."
|
|
78
|
+
|
|
79
|
+
## Constraints
|
|
80
|
+
|
|
81
|
+
- DO NOT spend more than 30 seconds on this reflection
|
|
82
|
+
- Be specific and actionable in your improvement suggestions
|
|
83
|
+
- Only suggest improvements that would make a meaningful difference
|
|
84
|
+
- If you're unsure, err on the side of "no improvements needed"
|
|
85
|
+
PROMPT
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../utils/workspace_rules"
|
|
4
|
+
|
|
5
|
+
module Octo
|
|
6
|
+
class Agent
|
|
7
|
+
# System prompt construction
|
|
8
|
+
# Builds system prompt by composing layers:
|
|
9
|
+
# 1. Agent-specific system_prompt.md (role & responsibilities)
|
|
10
|
+
# 2. base_prompt.md (universal rules: todo manager, tool usage, etc.)
|
|
11
|
+
# 3. Project rules (.octorules / .cursorrules / CLAUDE.md)
|
|
12
|
+
# 4. SOUL.md (agent personality — user override or built-in default)
|
|
13
|
+
# 5. USER.md (user profile — user override or built-in default)
|
|
14
|
+
# 6. Skills context (available skills list)
|
|
15
|
+
module SystemPromptBuilder
|
|
16
|
+
# Max characters loaded from each agent file (SOUL.md / USER.md)
|
|
17
|
+
MAX_MEMORY_FILE_CHARS = 1000
|
|
18
|
+
|
|
19
|
+
# Build complete system prompt with project rules and skills
|
|
20
|
+
# @return [String] Complete system prompt
|
|
21
|
+
def build_system_prompt
|
|
22
|
+
parts = []
|
|
23
|
+
|
|
24
|
+
# Layer 1: agent-specific role & responsibilities
|
|
25
|
+
parts << @agent_profile.system_prompt
|
|
26
|
+
|
|
27
|
+
# Layer 2: universal behavioral rules (todo manager, tool usage, etc.)
|
|
28
|
+
base = @agent_profile.base_prompt
|
|
29
|
+
parts << base unless base.empty?
|
|
30
|
+
|
|
31
|
+
# Layer 3: project-specific rules from working directory
|
|
32
|
+
project_rules = load_project_rules
|
|
33
|
+
if project_rules
|
|
34
|
+
parts << format_section("PROJECT-SPECIFIC RULES (from #{project_rules[:source]})",
|
|
35
|
+
project_rules[:content],
|
|
36
|
+
footer: "IMPORTANT: Follow these project-specific rules at all times!")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Layer 4 & 5: SOUL.md and USER.md (with built-in defaults as fallback)
|
|
40
|
+
soul = truncate(@agent_profile.soul, MAX_MEMORY_FILE_CHARS)
|
|
41
|
+
parts << format_section("AGENT SOUL (from ~/.octo/agents/SOUL.md)", soul) unless soul.empty?
|
|
42
|
+
|
|
43
|
+
user_profile = truncate(@agent_profile.user_profile, MAX_MEMORY_FILE_CHARS)
|
|
44
|
+
parts << format_section("USER PROFILE (from ~/.octo/agents/USER.md)", user_profile) unless user_profile.empty?
|
|
45
|
+
|
|
46
|
+
# Layer 6: skills context
|
|
47
|
+
skill_context = build_skill_context
|
|
48
|
+
parts << skill_context if skill_context && !skill_context.empty?
|
|
49
|
+
|
|
50
|
+
parts.join("\n\n")
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private def load_project_rules
|
|
54
|
+
main = Utils::WorkspaceRules.find_main(@working_dir)
|
|
55
|
+
sub_projects = Utils::WorkspaceRules.find_sub_projects(@working_dir)
|
|
56
|
+
|
|
57
|
+
return nil if main.nil? && sub_projects.empty?
|
|
58
|
+
|
|
59
|
+
combined_content = []
|
|
60
|
+
combined_content << main[:content] if main
|
|
61
|
+
|
|
62
|
+
unless sub_projects.empty?
|
|
63
|
+
n = Utils::WorkspaceRules::SUB_PROJECT_SUMMARY_LINES
|
|
64
|
+
summaries = sub_projects.map do |sp|
|
|
65
|
+
<<~SECTION.strip
|
|
66
|
+
### Sub-project: #{sp[:sub_name]}/
|
|
67
|
+
Summary (first #{n} lines of #{sp[:relative_path]}):
|
|
68
|
+
#{sp[:summary]}
|
|
69
|
+
> IMPORTANT: Before working on any files under #{sp[:sub_name]}/, read the full rules file at `#{sp[:relative_path]}` using file_reader.
|
|
70
|
+
SECTION
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
combined_content << <<~BLOCK.strip
|
|
74
|
+
## SUB-PROJECT AGENTS
|
|
75
|
+
This workspace contains sub-projects, each with their own rules.
|
|
76
|
+
When working in a sub-project, you MUST read its full .octorules first.
|
|
77
|
+
|
|
78
|
+
#{summaries.join("\n\n")}
|
|
79
|
+
BLOCK
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
source = main ? main[:name] : "sub-projects"
|
|
83
|
+
{ content: combined_content.join("\n\n"), source: source }
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private def format_section(title, content, footer: nil)
|
|
87
|
+
sep = "=" * 80
|
|
88
|
+
lines = ["", sep, title, sep, content, sep]
|
|
89
|
+
lines << footer if footer
|
|
90
|
+
lines << sep if footer
|
|
91
|
+
lines.join("\n")
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private def truncate(text, max_chars)
|
|
95
|
+
return text if text.length <= max_chars
|
|
96
|
+
|
|
97
|
+
text[0, max_chars] + "\n... [truncated]"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Octo
|
|
4
|
+
class Agent
|
|
5
|
+
# Time Machine module for task history management with undo/redo support
|
|
6
|
+
# Stores complete file snapshots (AFTER state) to support message compression
|
|
7
|
+
module TimeMachine
|
|
8
|
+
# Initialize Time Machine state
|
|
9
|
+
private def init_time_machine
|
|
10
|
+
@task_parents ||= {} # { task_id => parent_id }
|
|
11
|
+
@current_task_id ||= 0 # Latest created task ID
|
|
12
|
+
@active_task_id ||= 0 # Current active task ID (for undo/redo)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Start a new task and establish parent relationship
|
|
16
|
+
# Made public for testing
|
|
17
|
+
def start_new_task
|
|
18
|
+
parent_id = @active_task_id
|
|
19
|
+
@current_task_id += 1
|
|
20
|
+
@active_task_id = @current_task_id
|
|
21
|
+
@task_parents[@current_task_id] = parent_id
|
|
22
|
+
|
|
23
|
+
# Claim ownership of this task for the current thread.
|
|
24
|
+
# If a stale thread (e.g. a slow subagent) wakes up later it will see
|
|
25
|
+
# @task_thread != Thread.current via check_stale! and self-terminate
|
|
26
|
+
# before it can write to history.
|
|
27
|
+
@task_thread = Thread.current
|
|
28
|
+
|
|
29
|
+
@current_task_id
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Save snapshots of modified files (AFTER state)
|
|
33
|
+
# @param modified_files [Array<String>] List of file paths that were modified
|
|
34
|
+
# Made public for testing
|
|
35
|
+
def save_modified_files_snapshot(modified_files)
|
|
36
|
+
return if modified_files.nil? || modified_files.empty?
|
|
37
|
+
|
|
38
|
+
snapshot_dir = File.join(
|
|
39
|
+
Dir.home,
|
|
40
|
+
".octo",
|
|
41
|
+
"snapshots",
|
|
42
|
+
@session_id,
|
|
43
|
+
"task-#{@current_task_id}"
|
|
44
|
+
)
|
|
45
|
+
FileUtils.mkdir_p(snapshot_dir)
|
|
46
|
+
|
|
47
|
+
modified_files.each do |file_path|
|
|
48
|
+
next unless File.exist?(file_path)
|
|
49
|
+
|
|
50
|
+
# Save file content to snapshot
|
|
51
|
+
relative_path = file_path.start_with?(@working_dir) ?
|
|
52
|
+
file_path.sub(@working_dir + "/", "") : File.basename(file_path)
|
|
53
|
+
|
|
54
|
+
snapshot_file = File.join(snapshot_dir, relative_path)
|
|
55
|
+
FileUtils.mkdir_p(File.dirname(snapshot_file))
|
|
56
|
+
FileUtils.cp(file_path, snapshot_file)
|
|
57
|
+
end
|
|
58
|
+
rescue StandardError => e
|
|
59
|
+
# Silently handle errors in tests
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Restore files to the state at given task
|
|
63
|
+
# @param task_id [Integer] Target task ID
|
|
64
|
+
# Made public for testing
|
|
65
|
+
def restore_to_task_state(task_id)
|
|
66
|
+
# Collect all modified files from task 1 to target task
|
|
67
|
+
files_to_restore = {}
|
|
68
|
+
|
|
69
|
+
(1..task_id).each do |tid|
|
|
70
|
+
snapshot_dir = File.join(
|
|
71
|
+
Dir.home,
|
|
72
|
+
".octo",
|
|
73
|
+
"snapshots",
|
|
74
|
+
@session_id,
|
|
75
|
+
"task-#{tid}"
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
next unless Dir.exist?(snapshot_dir)
|
|
79
|
+
|
|
80
|
+
Dir.glob(File.join(snapshot_dir, "**", "*")).each do |snapshot_file|
|
|
81
|
+
next if File.directory?(snapshot_file)
|
|
82
|
+
|
|
83
|
+
relative_path = snapshot_file.sub(snapshot_dir + "/", "")
|
|
84
|
+
files_to_restore[relative_path] = snapshot_file
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Restore files
|
|
89
|
+
files_to_restore.each do |relative_path, snapshot_file|
|
|
90
|
+
target_file = File.join(@working_dir, relative_path)
|
|
91
|
+
FileUtils.mkdir_p(File.dirname(target_file))
|
|
92
|
+
FileUtils.cp(snapshot_file, target_file)
|
|
93
|
+
end
|
|
94
|
+
rescue StandardError => e
|
|
95
|
+
# Silently handle errors in tests
|
|
96
|
+
raise
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Filter messages to only show tasks up to active_task_id.
|
|
100
|
+
# This hides "future" messages when user has undone.
|
|
101
|
+
# Returns API-ready array (strips internal fields + handles orphaned tool_calls).
|
|
102
|
+
# @param force_reasoning_content_pad [Boolean] forwarded to MessageHistory,
|
|
103
|
+
# enables one-shot pad-and-retry for thinking-mode providers that
|
|
104
|
+
# require reasoning_content on every assistant message.
|
|
105
|
+
# Made public for testing
|
|
106
|
+
def active_messages(force_reasoning_content_pad: false)
|
|
107
|
+
if @active_task_id == @current_task_id
|
|
108
|
+
return @history.to_api(force_reasoning_content_pad: force_reasoning_content_pad)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
stripped = @history.for_task(@active_task_id).map do |msg|
|
|
112
|
+
msg.reject { |k, _| MessageHistory::INTERNAL_FIELDS.include?(k) }
|
|
113
|
+
end
|
|
114
|
+
# Apply the same reasoning_content padding rule used by to_api so
|
|
115
|
+
# Time Machine replays satisfy thinking-mode providers after a
|
|
116
|
+
# 400 retry.
|
|
117
|
+
MessageHistory.pad_reasoning_content_if_needed(stripped, force: force_reasoning_content_pad)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Undo to parent task
|
|
121
|
+
def undo_last_task
|
|
122
|
+
parent_id = @task_parents[@active_task_id]
|
|
123
|
+
return { success: false, message: "Already at root task" } if parent_id.nil? || parent_id == 0
|
|
124
|
+
|
|
125
|
+
restore_to_task_state(parent_id)
|
|
126
|
+
@active_task_id = parent_id
|
|
127
|
+
|
|
128
|
+
{
|
|
129
|
+
success: true,
|
|
130
|
+
message: "⏪ Undone to task #{parent_id}",
|
|
131
|
+
task_id: parent_id
|
|
132
|
+
}
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
# Switch to specific task (for redo or branch switching)
|
|
136
|
+
def switch_to_task(target_task_id)
|
|
137
|
+
if target_task_id > @current_task_id || target_task_id < 1
|
|
138
|
+
return { success: false, message: "Invalid task ID: #{target_task_id}" }
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
restore_to_task_state(target_task_id)
|
|
142
|
+
@active_task_id = target_task_id
|
|
143
|
+
|
|
144
|
+
{
|
|
145
|
+
success: true,
|
|
146
|
+
message: "⏩ Switched to task #{target_task_id}",
|
|
147
|
+
task_id: target_task_id
|
|
148
|
+
}
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
# Get children of a task (for branch detection)
|
|
152
|
+
def get_child_tasks(task_id)
|
|
153
|
+
@task_parents.select { |_, parent| parent == task_id }.keys
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Get task history with summaries for UI display
|
|
157
|
+
# @param limit [Integer] Maximum number of recent tasks to return
|
|
158
|
+
# @return [Array<Hash>] Task history with metadata
|
|
159
|
+
def get_task_history(limit: 10)
|
|
160
|
+
return [] if @current_task_id == 0
|
|
161
|
+
|
|
162
|
+
tasks = []
|
|
163
|
+
(1..@current_task_id).to_a.reverse.take(limit).reverse.each do |task_id|
|
|
164
|
+
# Find first user message for this task
|
|
165
|
+
first_user_msg = @history.to_a.find do |msg|
|
|
166
|
+
msg[:task_id] == task_id && msg[:role] == "user"
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
summary = if first_user_msg
|
|
170
|
+
content = extract_message_text(first_user_msg[:content])
|
|
171
|
+
# Truncate to 60 characters (including "...")
|
|
172
|
+
content.length > 60 ? "#{content[0...57]}..." : content
|
|
173
|
+
else
|
|
174
|
+
"Task #{task_id}"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
# Determine task status
|
|
178
|
+
status = if task_id == @active_task_id
|
|
179
|
+
:current
|
|
180
|
+
elsif task_id < @active_task_id
|
|
181
|
+
:past
|
|
182
|
+
else
|
|
183
|
+
:future
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Check if task has branches (multiple children)
|
|
187
|
+
children = get_child_tasks(task_id)
|
|
188
|
+
has_branches = children.length > 1
|
|
189
|
+
|
|
190
|
+
tasks << {
|
|
191
|
+
task_id: task_id,
|
|
192
|
+
summary: summary,
|
|
193
|
+
status: status,
|
|
194
|
+
has_branches: has_branches
|
|
195
|
+
}
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
tasks
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# Extract text from message content (handles both string and array formats)
|
|
202
|
+
private def extract_message_text(content)
|
|
203
|
+
if content.is_a?(String)
|
|
204
|
+
content
|
|
205
|
+
elsif content.is_a?(Array)
|
|
206
|
+
text_parts = content.select { |part| part[:type] == "text" }
|
|
207
|
+
text_parts.map { |part| part[:text] }.join(" ")
|
|
208
|
+
else
|
|
209
|
+
""
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|