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,246 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Octo
|
|
4
|
+
class Agent
|
|
5
|
+
# Long-term memory update functionality.
|
|
6
|
+
#
|
|
7
|
+
# Runs at the end of a qualifying task to persist important knowledge
|
|
8
|
+
# into ~/.octo/memories/. The LLM decides:
|
|
9
|
+
# - Which topics were discussed
|
|
10
|
+
# - Which memory files to update or create
|
|
11
|
+
# - How to merge new info with existing content
|
|
12
|
+
# - What to drop to stay within the per-file token limit
|
|
13
|
+
#
|
|
14
|
+
# Architecture:
|
|
15
|
+
# Memory update runs as a **forked subagent**, NOT inline in the
|
|
16
|
+
# main agent's loop. The subagent inherits the main agent's history
|
|
17
|
+
# (so it can see what happened) via +fork_subagent+'s standard
|
|
18
|
+
# deep-clone, and inherits the same model/tools so prompt-cache is
|
|
19
|
+
# reused maximally. The subagent runs synchronously; when it returns,
|
|
20
|
+
# the main agent prints +show_complete+.
|
|
21
|
+
#
|
|
22
|
+
# This gives us, structurally:
|
|
23
|
+
# - Clean main-agent history (no memory_update messages to clean up)
|
|
24
|
+
# - Correct visual ordering ([OK] Task Complete is the LAST thing
|
|
25
|
+
# printed — the memory-update progress finishes before it)
|
|
26
|
+
# - Independent cost accounting (task cost vs. memory update cost)
|
|
27
|
+
# - Natural recursion guard (+@is_subagent+ blocks re-entry)
|
|
28
|
+
#
|
|
29
|
+
# Trigger condition:
|
|
30
|
+
# - Iteration count >= MEMORY_UPDATE_MIN_ITERATIONS (skip trivial tasks)
|
|
31
|
+
# - Not already a subagent (no recursion)
|
|
32
|
+
# - Memory update is enabled in config
|
|
33
|
+
module MemoryUpdater
|
|
34
|
+
# Minimum LLM iterations for this task before triggering memory update.
|
|
35
|
+
# Set high enough to skip short utility tasks (commit, deploy, etc.)
|
|
36
|
+
MEMORY_UPDATE_MIN_ITERATIONS = 10
|
|
37
|
+
|
|
38
|
+
MEMORIES_DIR = File.expand_path("~/.octo/memories")
|
|
39
|
+
|
|
40
|
+
# Check if memory update should be triggered for this task.
|
|
41
|
+
# Only triggers when the task had enough LLM iterations,
|
|
42
|
+
# skipping short utility tasks (e.g. commit, deploy).
|
|
43
|
+
# @return [Boolean]
|
|
44
|
+
def should_update_memory?
|
|
45
|
+
return false unless memory_update_enabled?
|
|
46
|
+
return false if @is_subagent # Subagents never update memory
|
|
47
|
+
|
|
48
|
+
task_iterations = @iterations - (@task_start_iterations || 0)
|
|
49
|
+
task_iterations >= MEMORY_UPDATE_MIN_ITERATIONS
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Run memory update as a forked subagent.
|
|
53
|
+
#
|
|
54
|
+
# This is called by +Agent#run+ on the success path, AFTER the main
|
|
55
|
+
# loop exits and BEFORE +show_complete+ is printed. It blocks until
|
|
56
|
+
# the subagent finishes, so the visual order is structurally correct:
|
|
57
|
+
#
|
|
58
|
+
# ... task output ...
|
|
59
|
+
# [progress] Updating long-term memory… (spinner)
|
|
60
|
+
# [progress finishes]
|
|
61
|
+
# [OK] Task Complete
|
|
62
|
+
#
|
|
63
|
+
# Safe to call unconditionally; returns early if preconditions fail.
|
|
64
|
+
# Never raises for "no update needed" — only propagates genuine errors
|
|
65
|
+
# (+Octo::AgentInterrupted+ for Ctrl+C, other exceptions are caught
|
|
66
|
+
# and logged so memory-update failures never mask the parent task's
|
|
67
|
+
# result).
|
|
68
|
+
def run_memory_update_subagent
|
|
69
|
+
return unless should_update_memory?
|
|
70
|
+
|
|
71
|
+
handle = @ui&.start_progress(message: "Updating long-term memory…", style: :primary)
|
|
72
|
+
|
|
73
|
+
# Fork subagent inheriting main agent's model, tools, and history.
|
|
74
|
+
# Maximizes prompt-cache reuse: same model, same tool set, same
|
|
75
|
+
# cloned history — only the +system_prompt_suffix+ (the memory
|
|
76
|
+
# update instructions) and the final "Please proceed." user turn
|
|
77
|
+
# are new, landing on top of a warm cache.
|
|
78
|
+
subagent = fork_subagent(system_prompt_suffix: build_memory_update_prompt)
|
|
79
|
+
|
|
80
|
+
# Memory update is a background consolidation task — never prompt
|
|
81
|
+
# the user for confirmation on memory file writes. The subagent
|
|
82
|
+
# has its own config copy (fork_subagent does deep_copy), so this
|
|
83
|
+
# doesn't affect the parent.
|
|
84
|
+
sub_config = subagent.instance_variable_get(:@config)
|
|
85
|
+
sub_config.permission_mode = :auto_approve if sub_config.respond_to?(:permission_mode=)
|
|
86
|
+
|
|
87
|
+
begin
|
|
88
|
+
result = subagent.run("Please proceed.")
|
|
89
|
+
rescue Octo::AgentInterrupted
|
|
90
|
+
# User pressed Ctrl+C during memory update. Propagate so the
|
|
91
|
+
# parent agent's interrupt handler runs.
|
|
92
|
+
raise
|
|
93
|
+
rescue StandardError => e
|
|
94
|
+
# Memory update failures are NEVER fatal to the parent task.
|
|
95
|
+
# Log and move on — the user's actual work is already done.
|
|
96
|
+
@debug_logs << {
|
|
97
|
+
timestamp: Time.now.iso8601,
|
|
98
|
+
event: "memory_update_error",
|
|
99
|
+
error_class: e.class.name,
|
|
100
|
+
error_message: e.message,
|
|
101
|
+
backtrace: e.backtrace&.first(10)
|
|
102
|
+
}
|
|
103
|
+
Octo::Logger.error("memory_update_error", error: e)
|
|
104
|
+
return
|
|
105
|
+
ensure
|
|
106
|
+
handle&.finish
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
return unless result
|
|
110
|
+
|
|
111
|
+
# Only surface a completion info line if the subagent actually
|
|
112
|
+
# wrote something to memory. The common "No memory updates needed."
|
|
113
|
+
# path stays silent to avoid visual noise.
|
|
114
|
+
if subagent_wrote_memory?(subagent)
|
|
115
|
+
@ui&.show_info("Memory updated: #{result[:iterations]} iterations")
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private def memory_update_enabled?
|
|
120
|
+
# Check config flag; default to true if not set
|
|
121
|
+
return true unless @config.respond_to?(:memory_update_enabled)
|
|
122
|
+
|
|
123
|
+
@config.memory_update_enabled != false
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Inspect the subagent's history for a successful write/edit tool
|
|
127
|
+
# call targeting a memory file. Used to decide whether to surface a
|
|
128
|
+
# "Memory updated" info line (option C — silent when nothing changed).
|
|
129
|
+
# @param subagent [Octo::Agent]
|
|
130
|
+
# @return [Boolean]
|
|
131
|
+
private def subagent_wrote_memory?(subagent)
|
|
132
|
+
return false unless subagent.respond_to?(:history) && subagent.history
|
|
133
|
+
|
|
134
|
+
subagent.history.to_a.any? do |msg|
|
|
135
|
+
next false unless msg.is_a?(Hash)
|
|
136
|
+
|
|
137
|
+
# Match OpenAI-style tool_calls on assistant messages …
|
|
138
|
+
tool_calls = msg[:tool_calls] || msg["tool_calls"]
|
|
139
|
+
if tool_calls.is_a?(Array) && tool_calls.any?
|
|
140
|
+
next true if tool_calls.any? do |tc|
|
|
141
|
+
name = tc.dig(:function, :name) || tc.dig("function", "name") || tc[:name] || tc["name"]
|
|
142
|
+
%w[write edit].include?(name.to_s)
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# … and Anthropic-style content blocks with type=tool_use.
|
|
147
|
+
content = msg[:content] || msg["content"]
|
|
148
|
+
if content.is_a?(Array)
|
|
149
|
+
next true if content.any? do |block|
|
|
150
|
+
block.is_a?(Hash) &&
|
|
151
|
+
(block[:type] == "tool_use" || block["type"] == "tool_use") &&
|
|
152
|
+
%w[write edit].include?((block[:name] || block["name"]).to_s)
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
false
|
|
157
|
+
end
|
|
158
|
+
rescue StandardError
|
|
159
|
+
# Defensive: never let introspection errors break memory update.
|
|
160
|
+
false
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
# Build the memory update prompt for the forked subagent.
|
|
164
|
+
#
|
|
165
|
+
# Architecture:
|
|
166
|
+
# - Decision (whitelist) lives HERE — MemoryUpdater is the trigger
|
|
167
|
+
# and decides whether/what to persist.
|
|
168
|
+
# - Execution (file naming, merging, frontmatter, size limits) lives
|
|
169
|
+
# in the persist-memory skill — MemoryUpdater loads SKILL.md
|
|
170
|
+
# directly via SkillManager and embeds it as the executor manual.
|
|
171
|
+
#
|
|
172
|
+
# We do NOT call invoke_skill here (that would fork a second
|
|
173
|
+
# subagent — the persist-memory skill is fork_agent:true). Instead
|
|
174
|
+
# the subagent we already forked plays both roles: it reads the
|
|
175
|
+
# whitelist, decides what (if anything) to persist, and follows
|
|
176
|
+
# the embedded SKILL.md rules to write the files.
|
|
177
|
+
#
|
|
178
|
+
# @return [String]
|
|
179
|
+
private def build_memory_update_prompt
|
|
180
|
+
executor_manual = load_persist_memory_skill_body
|
|
181
|
+
|
|
182
|
+
<<~PROMPT
|
|
183
|
+
═══════════════════════════════════════════════════════════════
|
|
184
|
+
MEMORY UPDATE MODE
|
|
185
|
+
═══════════════════════════════════════════════════════════════
|
|
186
|
+
The conversation above has ended. You are now in MEMORY UPDATE MODE.
|
|
187
|
+
|
|
188
|
+
## Default: Do NOT write anything.
|
|
189
|
+
|
|
190
|
+
Memory writes are expensive. Only write if the session contains at least one of the
|
|
191
|
+
following high-value signals. If NONE apply, respond immediately with:
|
|
192
|
+
"No memory updates needed." and STOP — do not use any tools.
|
|
193
|
+
|
|
194
|
+
## Whitelist: Write ONLY if at least one condition is met
|
|
195
|
+
|
|
196
|
+
1. **Explicit decision** — The user made a clear technical, product, or process decision
|
|
197
|
+
that will affect future work (e.g. "we'll use X instead of Y going forward").
|
|
198
|
+
2. **New persistent context** — The user introduced project background, constraints, or
|
|
199
|
+
goals that are not already obvious from the code (e.g. a new feature direction,
|
|
200
|
+
a deployment target, a team convention).
|
|
201
|
+
3. **Correction of prior knowledge** — The user corrected a previous misunderstanding
|
|
202
|
+
or the agent discovered that an existing memory is wrong or outdated.
|
|
203
|
+
4. **Stated preference** — The user expressed a clear personal or team preference about
|
|
204
|
+
how they want the agent to behave, communicate, or write code.
|
|
205
|
+
|
|
206
|
+
## What does NOT qualify (skip these entirely)
|
|
207
|
+
|
|
208
|
+
- Running tests, fixing lint, formatting code
|
|
209
|
+
- Committing, deploying, or releasing
|
|
210
|
+
- Answering a one-off question or explaining a concept
|
|
211
|
+
- Any task that produced no lasting decisions or preferences
|
|
212
|
+
- Repeating or slightly rephrasing what is already in memory
|
|
213
|
+
|
|
214
|
+
═══════════════════════════════════════════════════════════════
|
|
215
|
+
EXECUTOR MANUAL (from persist-memory skill)
|
|
216
|
+
═══════════════════════════════════════════════════════════════
|
|
217
|
+
If — and ONLY if — the whitelist matched, follow the manual below
|
|
218
|
+
to actually write the files. The manual owns file naming, merging,
|
|
219
|
+
frontmatter, and size limits. Treat it as authoritative for
|
|
220
|
+
execution; ignore any "should I write?" framing inside it (that
|
|
221
|
+
decision has already been made above).
|
|
222
|
+
|
|
223
|
+
#{executor_manual}
|
|
224
|
+
|
|
225
|
+
───────────────────────────────────────────────────────────────
|
|
226
|
+
Begin by checking the whitelist. If no condition is met, stop immediately.
|
|
227
|
+
PROMPT
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Load the persist-memory skill's expanded body (frontmatter stripped,
|
|
231
|
+
# template variables like <%= memories_meta %> resolved).
|
|
232
|
+
#
|
|
233
|
+
# The persist-memory skill is a built-in default skill — it is always
|
|
234
|
+
# present. If it isn't, that's a build/install bug and we want it to
|
|
235
|
+
# surface loudly rather than silently degrade.
|
|
236
|
+
#
|
|
237
|
+
# @return [String]
|
|
238
|
+
private def load_persist_memory_skill_body
|
|
239
|
+
skill = @skill_loader.find_by_name("persist-memory")
|
|
240
|
+
raise "persist-memory skill not found — built-in skill is missing" unless skill
|
|
241
|
+
|
|
242
|
+
skill.process_content(template_context: build_template_context)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Octo
|
|
4
|
+
# Message compressor using Insert-then-Compress strategy
|
|
5
|
+
#
|
|
6
|
+
# New Strategy: Instead of creating a separate API call for compression,
|
|
7
|
+
# we insert a compression instruction into the current conversation flow.
|
|
8
|
+
# This allows us to reuse the existing cache (system prompt + tools) and
|
|
9
|
+
# only pay for processing the new compression instruction.
|
|
10
|
+
#
|
|
11
|
+
# Flow:
|
|
12
|
+
# 1. Agent detects compression threshold is reached
|
|
13
|
+
# 2. Compressor builds a compression instruction message
|
|
14
|
+
# 3. Agent inserts this message and calls LLM (with cache reuse!)
|
|
15
|
+
# 4. LLM returns compressed summary
|
|
16
|
+
# 5. Compressor rebuilds message list: system + summary + recent messages
|
|
17
|
+
# 6. Agent continues with new message list (cache will rebuild from here)
|
|
18
|
+
#
|
|
19
|
+
# Benefits:
|
|
20
|
+
# - Compression call reuses existing cache (huge token savings)
|
|
21
|
+
# - Only one cache rebuild after compression (vs two with old approach)
|
|
22
|
+
#
|
|
23
|
+
class MessageCompressor
|
|
24
|
+
COMPRESSION_PROMPT = <<~PROMPT.freeze
|
|
25
|
+
═══════════════════════════════════════════════════════════════
|
|
26
|
+
CRITICAL: TASK CHANGE - MEMORY COMPRESSION MODE
|
|
27
|
+
═══════════════════════════════════════════════════════════════
|
|
28
|
+
The conversation above has ENDED. You are now in MEMORY COMPRESSION MODE.
|
|
29
|
+
|
|
30
|
+
CRITICAL INSTRUCTIONS - READ CAREFULLY:
|
|
31
|
+
|
|
32
|
+
1. This is NOT a continuation of the conversation
|
|
33
|
+
2. DO NOT respond to any requests in the conversation above
|
|
34
|
+
3. DO NOT call ANY tools or functions
|
|
35
|
+
4. DO NOT use tool_calls in your response
|
|
36
|
+
5. Your response MUST be PURE TEXT ONLY
|
|
37
|
+
|
|
38
|
+
YOUR ONLY TASK: Create a comprehensive summary of the conversation above.
|
|
39
|
+
|
|
40
|
+
REQUIRED RESPONSE FORMAT:
|
|
41
|
+
First output a <topics> line listing 3-6 key topic phrases (comma-separated, concise).
|
|
42
|
+
Then output the full summary wrapped in <summary> tags.
|
|
43
|
+
|
|
44
|
+
Example format:
|
|
45
|
+
<topics>Rails setup, database config, deploy pipeline, Tailwind CSS</topics>
|
|
46
|
+
<summary>
|
|
47
|
+
...full summary text...
|
|
48
|
+
</summary>
|
|
49
|
+
|
|
50
|
+
Focus on:
|
|
51
|
+
- User's explicit requests and intents
|
|
52
|
+
- Key technical concepts and code changes
|
|
53
|
+
- Files examined and modified
|
|
54
|
+
- Errors encountered and fixes applied
|
|
55
|
+
- Current work status and pending tasks
|
|
56
|
+
|
|
57
|
+
Begin your response NOW. Remember: PURE TEXT only, starting with <topics> then <summary>.
|
|
58
|
+
PROMPT
|
|
59
|
+
|
|
60
|
+
def initialize(client, model: nil)
|
|
61
|
+
@client = client
|
|
62
|
+
@model = model
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Generate compression instruction message to be inserted into conversation
|
|
66
|
+
# This enables cache reuse by using the same API call with tools
|
|
67
|
+
#
|
|
68
|
+
# SIMPLIFIED APPROACH:
|
|
69
|
+
# - Don't duplicate conversation history in the compression message
|
|
70
|
+
# - LLM can already see all messages, just ask it to compress
|
|
71
|
+
# - Keep the instruction small for better cache efficiency
|
|
72
|
+
#
|
|
73
|
+
# @param messages [Array<Hash>] Original conversation messages
|
|
74
|
+
# @param recent_messages [Array<Hash>] Recent messages to keep uncompressed (optional)
|
|
75
|
+
# @return [Hash] Compression instruction message to insert, or nil if nothing to compress
|
|
76
|
+
def build_compression_message(messages, recent_messages: [])
|
|
77
|
+
# Get messages to compress (exclude system message and recent messages)
|
|
78
|
+
messages_to_compress = messages.reject { |m| m[:role] == "system" || recent_messages.include?(m) }
|
|
79
|
+
|
|
80
|
+
# If nothing to compress, return nil
|
|
81
|
+
return nil if messages_to_compress.empty?
|
|
82
|
+
|
|
83
|
+
# Simple compression instruction - LLM can see the history already
|
|
84
|
+
{
|
|
85
|
+
role: "user",
|
|
86
|
+
content: COMPRESSION_PROMPT,
|
|
87
|
+
system_injected: true
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Parse LLM response and rebuild message list with compression
|
|
92
|
+
# @param compressed_content [String] The compressed summary from LLM
|
|
93
|
+
# @param original_messages [Array<Hash>] Original messages before compression
|
|
94
|
+
# @param recent_messages [Array<Hash>] Recent messages to preserve
|
|
95
|
+
# @param chunk_path [String, nil] Path to the archived chunk MD file (if saved)
|
|
96
|
+
# @param pulled_back_messages [Array<Hash>] Messages temporarily popped from the
|
|
97
|
+
# tail of @history before the compression LLM call (to free up token budget so
|
|
98
|
+
# the compression call itself doesn't overflow context). These are NOT discarded —
|
|
99
|
+
# they are reattached to the tail of the rebuilt history so recent task progress
|
|
100
|
+
# is preserved. Default: [] (normal compression path doesn't need this).
|
|
101
|
+
# @return [Array<Hash>] Rebuilt message list: system + compressed + recent + pulled_back
|
|
102
|
+
def rebuild_with_compression(compressed_content, original_messages:, recent_messages:, chunk_path: nil, topics: nil, previous_chunks: [], pulled_back_messages: [])
|
|
103
|
+
# Find and preserve system message
|
|
104
|
+
system_msg = original_messages.find { |m| m[:role] == "system" }
|
|
105
|
+
|
|
106
|
+
# Parse the compressed result, embedding previous chunk references so the
|
|
107
|
+
# new summary carries a complete index of all older archives. This avoids
|
|
108
|
+
# keeping all prior compressed_summary messages in active history while
|
|
109
|
+
# still giving the AI a path to find old conversations via file_reader.
|
|
110
|
+
parsed_messages = parse_compressed_result(compressed_content,
|
|
111
|
+
chunk_path: chunk_path,
|
|
112
|
+
topics: topics,
|
|
113
|
+
previous_chunks: previous_chunks)
|
|
114
|
+
|
|
115
|
+
# If parsing fails or returns empty, raise error
|
|
116
|
+
if parsed_messages.nil? || parsed_messages.empty?
|
|
117
|
+
raise "LLM compression failed: unable to parse compressed messages"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Return system message + compressed messages + recent messages + pulled_back messages.
|
|
121
|
+
# Strip any system messages from recent_messages as a safety net —
|
|
122
|
+
# get_recent_messages_with_tool_pairs already excludes them, but this
|
|
123
|
+
# guard ensures we never end up with duplicate system prompts even if
|
|
124
|
+
# the caller passes an unfiltered list.
|
|
125
|
+
#
|
|
126
|
+
# pulled_back_messages: messages that were temporarily popped from the tail
|
|
127
|
+
# of @history before the compression LLM call (to free up token budget so
|
|
128
|
+
# the compression call itself doesn't overflow context). They are reattached
|
|
129
|
+
# here to preserve recent task progress.
|
|
130
|
+
safe_recent = recent_messages.reject { |m| m[:role] == "system" }
|
|
131
|
+
safe_pulled_back = pulled_back_messages.reject { |m| m[:role] == "system" }
|
|
132
|
+
[system_msg, *parsed_messages, *safe_recent, *safe_pulled_back].compact
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
# Parse topics tag from compressed content.
|
|
137
|
+
# Returns the topics string if found, nil otherwise.
|
|
138
|
+
# e.g. "<topics>Rails setup, database config</topics>" → "Rails setup, database config"
|
|
139
|
+
def parse_topics(content)
|
|
140
|
+
m = content.match(/<topics>(.*?)<\/topics>/m)
|
|
141
|
+
m ? m[1].strip : nil
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def parse_compressed_result(result, chunk_path: nil, topics: nil, previous_chunks: [])
|
|
145
|
+
# Return the compressed result as a single user message (role: "user").
|
|
146
|
+
#
|
|
147
|
+
# Why role:"user" instead of "assistant":
|
|
148
|
+
# When all original user messages get archived into the chunk during compression
|
|
149
|
+
# (e.g. a long single-turn `/slash` task), the rebuilt history can end up as
|
|
150
|
+
# `system → assistant(summary) → assistant(tool_calls) → tool → …` with NO user
|
|
151
|
+
# message anywhere. Strict providers (notably DeepSeek V4 thinking mode) reject
|
|
152
|
+
# this as a malformed turn structure with a misleading
|
|
153
|
+
# "reasoning_content must be passed back" 400 error.
|
|
154
|
+
#
|
|
155
|
+
# Marking it as a user message gives the conversation a valid turn boundary.
|
|
156
|
+
# `system_injected: true` ensures the UI's replay_history still hides it from
|
|
157
|
+
# the chat panel (the real-user filter excludes system_injected messages), while
|
|
158
|
+
# INTERNAL_FIELDS in MessageHistory strips the marker before the API payload is
|
|
159
|
+
# built — so DeepSeek/OpenAI/Anthropic only see a plain `{role:"user", content:…}`.
|
|
160
|
+
#
|
|
161
|
+
# The `compressed_summary: true` flag is preserved so that replay_history still
|
|
162
|
+
# routes this message through the chunk-expansion path (which keys off that flag,
|
|
163
|
+
# not the role).
|
|
164
|
+
#
|
|
165
|
+
# @param topics [String, nil] Short topic description extracted from <topics> tag
|
|
166
|
+
# @param previous_chunks [Array<Hash>] Info about older chunk files
|
|
167
|
+
# Each hash: { basename:, path:, topics: }
|
|
168
|
+
content = result.to_s.strip
|
|
169
|
+
|
|
170
|
+
if content.empty?
|
|
171
|
+
[]
|
|
172
|
+
else
|
|
173
|
+
# Strip out the <topics> block — it's metadata for the chunk file, not for AI context
|
|
174
|
+
content_without_topics = content.gsub(/<topics>.*?<\/topics>\n*/m, "").strip
|
|
175
|
+
|
|
176
|
+
# Build previous chunks index section — links to older chunk files so the AI
|
|
177
|
+
# can find earlier conversations without keeping all prior compressed_summary
|
|
178
|
+
# messages in the active history. Shows newest chunks first (reverse order),
|
|
179
|
+
# capped at 10 to keep the message size bounded.
|
|
180
|
+
previous_chunks_section = ""
|
|
181
|
+
if previous_chunks.any?
|
|
182
|
+
max_visible = 10
|
|
183
|
+
visible = previous_chunks.last(max_visible).reverse
|
|
184
|
+
older_count = previous_chunks.size - visible.size
|
|
185
|
+
|
|
186
|
+
previous_chunks_section = "\n\n---\n📁 **Previous chunks (newest first):**\n"
|
|
187
|
+
visible.each do |pc|
|
|
188
|
+
topic_str = pc[:topics] ? " — #{pc[:topics]}" : ""
|
|
189
|
+
previous_chunks_section += "- `#{pc[:basename]}`#{topic_str}\n"
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
if older_count > 0
|
|
193
|
+
oldest = previous_chunks.first
|
|
194
|
+
previous_chunks_section += "- ... and #{older_count} older chunks back to `#{oldest[:basename]}`\n"
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
previous_chunks_section += "_Use `file_reader` to recall details from these chunks._"
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Inject chunk anchor so AI knows where to find original conversation for THIS chunk
|
|
201
|
+
anchor = ""
|
|
202
|
+
if chunk_path
|
|
203
|
+
anchor = "\n\n---\n📁 **Current chunk archived at:** `#{chunk_path}`\n" \
|
|
204
|
+
"_Use `file_reader` tool to recall details from this chunk._"
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
# Prefix lets the model recognise this is injected context, not a user utterance.
|
|
208
|
+
# Order: summary → previous chunks → current anchor (chronological)
|
|
209
|
+
framed_content = "[Compressed conversation summary — previous turns archived]\n\n" \
|
|
210
|
+
"#{content_without_topics}" \
|
|
211
|
+
"#{previous_chunks_section}" \
|
|
212
|
+
"#{anchor}"
|
|
213
|
+
|
|
214
|
+
[{
|
|
215
|
+
role: "user",
|
|
216
|
+
content: framed_content,
|
|
217
|
+
compressed_summary: true,
|
|
218
|
+
chunk_path: chunk_path,
|
|
219
|
+
topics: topics,
|
|
220
|
+
system_injected: true
|
|
221
|
+
}]
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end
|
|
225
|
+
end
|