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,215 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Octo
|
|
4
|
+
class Agent
|
|
5
|
+
# Background "ghost text" prediction of the user's next message.
|
|
6
|
+
#
|
|
7
|
+
# Fired after each main-agent task completes. Generates one short phrase
|
|
8
|
+
# (the model's best guess at what the user will type next) and pushes it
|
|
9
|
+
# to the UI via +show_next_message_suggestion+. The web UI renders it as
|
|
10
|
+
# the input box's placeholder with Tab-to-accept; terminal / IM UIs are
|
|
11
|
+
# no-ops by default.
|
|
12
|
+
#
|
|
13
|
+
# Design notes:
|
|
14
|
+
# - NOT a forked subagent. Subagents clone history, run a full
|
|
15
|
+
# think/act/observe loop, and trigger hooks — overkill for "generate
|
|
16
|
+
# one line." We make a single +Client#send_messages+ call directly.
|
|
17
|
+
# - Async (own thread) and fire-and-forget. +Agent#show_complete+ must
|
|
18
|
+
# never block on the suggestion call.
|
|
19
|
+
# - Uses the provider's lite model when available (Claude → Haiku,
|
|
20
|
+
# DeepSeek pro → flash, ...), falling back to the current primary
|
|
21
|
+
# when no lite mapping exists.
|
|
22
|
+
# - Reuses the main agent's system prompt + last few messages as the
|
|
23
|
+
# LLM input. When primary and lite share a provider, this lands on a
|
|
24
|
+
# warm prompt cache; otherwise the cost is small absolute (short
|
|
25
|
+
# prompt, ≤40 output tokens).
|
|
26
|
+
# - Silent on any failure. A failed suggestion call never disturbs the
|
|
27
|
+
# user's actual task result.
|
|
28
|
+
module NextMessageSuggester
|
|
29
|
+
# Max characters in a suggestion that we'll forward to the UI. Anything
|
|
30
|
+
# longer is treated as a model misfire and dropped (it's supposed to be
|
|
31
|
+
# a phrase, not a paragraph).
|
|
32
|
+
MAX_SUGGESTION_CHARS = 80
|
|
33
|
+
|
|
34
|
+
# How many recent message pairs to send as context. 4 is plenty for the
|
|
35
|
+
# model to read the situation; more just inflates the cache footprint.
|
|
36
|
+
RECENT_HISTORY_LIMIT = 8
|
|
37
|
+
|
|
38
|
+
# Output budget — short phrases only.
|
|
39
|
+
SUGGESTION_MAX_TOKENS = 40
|
|
40
|
+
|
|
41
|
+
# Trigger predicate. Cheap; called on the agent thread.
|
|
42
|
+
def next_message_suggestion_enabled?
|
|
43
|
+
return false unless @config.respond_to?(:next_message_suggestion_enabled)
|
|
44
|
+
return false unless @config.next_message_suggestion_enabled
|
|
45
|
+
return false if @is_subagent
|
|
46
|
+
true
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Spawn the suggestion call in a daemon thread. Returns immediately.
|
|
50
|
+
def run_next_message_suggestion!
|
|
51
|
+
return unless next_message_suggestion_enabled?
|
|
52
|
+
return unless @ui
|
|
53
|
+
|
|
54
|
+
# Snapshot the agent state we need on the worker thread so we don't
|
|
55
|
+
# race with the next user turn mutating @history / @todos in place.
|
|
56
|
+
history_snapshot = recent_history_for_suggestion
|
|
57
|
+
return if history_snapshot.empty?
|
|
58
|
+
|
|
59
|
+
todos_snapshot = (@todos || []).map { |t| t.is_a?(Hash) ? t.dup : t }
|
|
60
|
+
ui = @ui
|
|
61
|
+
|
|
62
|
+
Thread.new do
|
|
63
|
+
text = generate_next_message_suggestion(history_snapshot, todos_snapshot)
|
|
64
|
+
ui.show_next_message_suggestion(text) if text && !text.empty?
|
|
65
|
+
rescue StandardError => e
|
|
66
|
+
Octo::Logger.warn(
|
|
67
|
+
"next_message_suggestion.failed",
|
|
68
|
+
session_id: @session_id,
|
|
69
|
+
error_class: e.class.name,
|
|
70
|
+
error_message: e.message
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private def generate_next_message_suggestion(history_snapshot, todos_snapshot)
|
|
76
|
+
client, model_name = build_suggestion_client_and_model
|
|
77
|
+
return nil unless client && model_name
|
|
78
|
+
|
|
79
|
+
messages = build_suggestion_messages(history_snapshot, todos_snapshot)
|
|
80
|
+
reply = client.send_messages(
|
|
81
|
+
messages,
|
|
82
|
+
model: model_name,
|
|
83
|
+
max_tokens: SUGGESTION_MAX_TOKENS
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
sanitize_suggestion(reply)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
# Pick the cheapest client/model that will give us a short reply.
|
|
90
|
+
# Prefers the provider's lite model so a Claude-Sonnet primary spawns
|
|
91
|
+
# a Haiku call; falls back to the primary client when no lite mapping
|
|
92
|
+
# exists for the current provider.
|
|
93
|
+
private def build_suggestion_client_and_model
|
|
94
|
+
lite_cfg = @config.respond_to?(:lite_model_config_for_current) ? @config.lite_model_config_for_current : nil
|
|
95
|
+
|
|
96
|
+
if lite_cfg && lite_cfg["model"] && lite_cfg["api_key"]
|
|
97
|
+
client = Octo::Client.new(
|
|
98
|
+
lite_cfg["api_key"],
|
|
99
|
+
base_url: lite_cfg["base_url"],
|
|
100
|
+
model: lite_cfg["model"],
|
|
101
|
+
anthropic_format: lite_cfg["anthropic_format"] || false
|
|
102
|
+
)
|
|
103
|
+
[client, lite_cfg["model"]]
|
|
104
|
+
else
|
|
105
|
+
# Fall back to the primary client. Costs more per token, but the
|
|
106
|
+
# prompt is short and the user explicitly hasn't configured a
|
|
107
|
+
# cheaper lite — respect their config.
|
|
108
|
+
[@client, @config.model_name]
|
|
109
|
+
end
|
|
110
|
+
rescue StandardError
|
|
111
|
+
[nil, nil]
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
private def build_suggestion_messages(history_snapshot, todos_snapshot)
|
|
115
|
+
[
|
|
116
|
+
{ role: "user", content: build_suggestion_prompt(history_snapshot, todos_snapshot) }
|
|
117
|
+
]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# We deliberately pass the prompt as a single user turn (not a
|
|
121
|
+
# system + chat-history rerun) for three reasons:
|
|
122
|
+
# 1. Some providers reject system-only prompts on the simple
|
|
123
|
+
# +send_messages+ path; staying within a plain user turn is
|
|
124
|
+
# universally portable.
|
|
125
|
+
# 2. Cache hit isn't realistic anyway — the suggestion model is
|
|
126
|
+
# usually a different model than the main one (Haiku vs Sonnet),
|
|
127
|
+
# so the cache wouldn't apply.
|
|
128
|
+
# 3. The prompt stays self-contained → trivial to log and replay.
|
|
129
|
+
private def build_suggestion_prompt(history_snapshot, todos_snapshot)
|
|
130
|
+
lines = []
|
|
131
|
+
lines << "You are a UI helper. Predict the user's next message in this conversation."
|
|
132
|
+
lines << ""
|
|
133
|
+
lines << "Output ONE short phrase (≤15 chars Chinese, ≤30 chars English)."
|
|
134
|
+
lines << "No quotes, no prefixes like 'User:'. Plain text only."
|
|
135
|
+
lines << "If background work is running or todos are in progress, prefer phrases like '等 X 完成' / 'wait for X'."
|
|
136
|
+
lines << "If you have no good guess, output the single token: NONE"
|
|
137
|
+
lines << ""
|
|
138
|
+
|
|
139
|
+
if todos_snapshot && !todos_snapshot.empty?
|
|
140
|
+
lines << "Current todos:"
|
|
141
|
+
todos_snapshot.first(6).each do |t|
|
|
142
|
+
status = t.is_a?(Hash) ? (t[:status] || t["status"]) : nil
|
|
143
|
+
content = t.is_a?(Hash) ? (t[:content] || t["content"]) : t.to_s
|
|
144
|
+
next unless content
|
|
145
|
+
lines << "- [#{status || "?"}] #{content.to_s[0, 80]}"
|
|
146
|
+
end
|
|
147
|
+
lines << ""
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
lines << "Recent conversation (oldest first):"
|
|
151
|
+
history_snapshot.each do |m|
|
|
152
|
+
role = m[:role] || m["role"]
|
|
153
|
+
content = m[:content] || m["content"]
|
|
154
|
+
next unless content
|
|
155
|
+
rendered = render_content_for_suggestion(content)
|
|
156
|
+
next if rendered.nil? || rendered.empty?
|
|
157
|
+
lines << "#{role}: #{rendered[0, 400]}"
|
|
158
|
+
end
|
|
159
|
+
lines << ""
|
|
160
|
+
lines << "Now output the predicted next user message:"
|
|
161
|
+
|
|
162
|
+
lines.join("\n")
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Pull the last few non-transient, non-tool messages from history.
|
|
166
|
+
# Excludes:
|
|
167
|
+
# - system messages (large, mostly noise for this prompt)
|
|
168
|
+
# - tool calls / results (verbose; the assistant message that
|
|
169
|
+
# surrounds them already conveys the gist)
|
|
170
|
+
# - system_injected user messages (file refs, compression hints)
|
|
171
|
+
private def recent_history_for_suggestion
|
|
172
|
+
all = @history.to_a
|
|
173
|
+
usable = all.reject do |m|
|
|
174
|
+
role = (m[:role] || m["role"]).to_s
|
|
175
|
+
role == "system" ||
|
|
176
|
+
role == "tool" ||
|
|
177
|
+
m[:system_injected] || m["system_injected"]
|
|
178
|
+
end
|
|
179
|
+
usable.last(RECENT_HISTORY_LIMIT)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
# Content in history can be a String OR an array of content blocks
|
|
183
|
+
# (Anthropic-style: [{type:"text", text:"..."}, {type:"image_url", ...}]).
|
|
184
|
+
# We only need text for the prompt.
|
|
185
|
+
private def render_content_for_suggestion(content)
|
|
186
|
+
case content
|
|
187
|
+
when String
|
|
188
|
+
content.strip
|
|
189
|
+
when Array
|
|
190
|
+
content.filter_map do |block|
|
|
191
|
+
text = block.is_a?(Hash) ? (block[:text] || block["text"]) : nil
|
|
192
|
+
text&.strip
|
|
193
|
+
end.join(" ").strip
|
|
194
|
+
else
|
|
195
|
+
nil
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
private def sanitize_suggestion(raw)
|
|
200
|
+
return nil if raw.nil?
|
|
201
|
+
text = raw.to_s.strip
|
|
202
|
+
return nil if text.empty?
|
|
203
|
+
# Take the first non-empty line — discard any chain-of-thought tail.
|
|
204
|
+
first_line = text.lines.map(&:strip).find { |l| !l.empty? }
|
|
205
|
+
return nil unless first_line
|
|
206
|
+
# Strip wrapping quotes the model sometimes adds.
|
|
207
|
+
first_line = first_line.gsub(/\A["'“”‘’`]+|["'“”‘’`]+\z/, "").strip
|
|
208
|
+
return nil if first_line.empty?
|
|
209
|
+
return nil if first_line.casecmp("NONE").zero?
|
|
210
|
+
return nil if first_line.length > MAX_SUGGESTION_CHARS
|
|
211
|
+
first_line
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|