claude_swarm 1.0.1 → 1.0.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.
Files changed (267) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/commands/release.md +1 -1
  3. data/.claude/hooks/lint-code-files.rb +65 -0
  4. data/.rubocop.yml +22 -2
  5. data/CHANGELOG.md +14 -1
  6. data/CLAUDE.md +1 -1
  7. data/CONTRIBUTING.md +69 -0
  8. data/README.md +27 -2
  9. data/Rakefile +71 -3
  10. data/analyze_coverage.rb +94 -0
  11. data/docs/v2/CHANGELOG.swarm_cli.md +43 -0
  12. data/docs/v2/CHANGELOG.swarm_memory.md +379 -0
  13. data/docs/v2/CHANGELOG.swarm_sdk.md +362 -0
  14. data/docs/v2/README.md +308 -0
  15. data/docs/v2/guides/claude-code-agents.md +262 -0
  16. data/docs/v2/guides/complete-tutorial.md +3088 -0
  17. data/docs/v2/guides/getting-started.md +1456 -0
  18. data/docs/v2/guides/memory-adapters.md +998 -0
  19. data/docs/v2/guides/plugins.md +816 -0
  20. data/docs/v2/guides/quick-start-cli.md +1745 -0
  21. data/docs/v2/guides/rails-integration.md +1902 -0
  22. data/docs/v2/guides/swarm-memory.md +599 -0
  23. data/docs/v2/reference/cli.md +729 -0
  24. data/docs/v2/reference/ruby-dsl.md +2154 -0
  25. data/docs/v2/reference/yaml.md +1835 -0
  26. data/docs-team-swarm.yml +2222 -0
  27. data/examples/learning-assistant/assistant.md +7 -0
  28. data/examples/learning-assistant/example-memories/concept-example.md +90 -0
  29. data/examples/learning-assistant/example-memories/experience-example.md +66 -0
  30. data/examples/learning-assistant/example-memories/fact-example.md +76 -0
  31. data/examples/learning-assistant/example-memories/memory-index.md +78 -0
  32. data/examples/learning-assistant/example-memories/skill-example.md +168 -0
  33. data/examples/learning-assistant/learning_assistant.rb +34 -0
  34. data/examples/learning-assistant/learning_assistant.yml +20 -0
  35. data/examples/v2/dsl/01_basic.rb +44 -0
  36. data/examples/v2/dsl/02_core_parameters.rb +59 -0
  37. data/examples/v2/dsl/03_capabilities.rb +71 -0
  38. data/examples/v2/dsl/04_llm_parameters.rb +56 -0
  39. data/examples/v2/dsl/05_advanced_flags.rb +73 -0
  40. data/examples/v2/dsl/06_permissions.rb +80 -0
  41. data/examples/v2/dsl/07_mcp_server.rb +62 -0
  42. data/examples/v2/dsl/08_swarm_hooks.rb +53 -0
  43. data/examples/v2/dsl/09_agent_hooks.rb +67 -0
  44. data/examples/v2/dsl/10_all_agents_hooks.rb +67 -0
  45. data/examples/v2/dsl/11_delegation.rb +60 -0
  46. data/examples/v2/dsl/12_complete_integration.rb +137 -0
  47. data/examples/v2/file_tools_swarm.yml +102 -0
  48. data/examples/v2/hooks/01_basic_hooks.rb +133 -0
  49. data/examples/v2/hooks/02_usage_tracking.rb +201 -0
  50. data/examples/v2/hooks/03_production_monitoring.rb +429 -0
  51. data/examples/v2/hooks/agent_stop_exit_0.yml +21 -0
  52. data/examples/v2/hooks/agent_stop_exit_1.yml +21 -0
  53. data/examples/v2/hooks/agent_stop_exit_2.yml +26 -0
  54. data/examples/v2/hooks/multiple_hooks_all_pass.yml +37 -0
  55. data/examples/v2/hooks/multiple_hooks_first_fails.yml +37 -0
  56. data/examples/v2/hooks/multiple_hooks_second_fails.yml +37 -0
  57. data/examples/v2/hooks/multiple_hooks_warnings.yml +37 -0
  58. data/examples/v2/hooks/post_tool_use_exit_0.yml +24 -0
  59. data/examples/v2/hooks/post_tool_use_exit_1.yml +24 -0
  60. data/examples/v2/hooks/post_tool_use_exit_2.yml +24 -0
  61. data/examples/v2/hooks/post_tool_use_multi_matcher_exit_0.yml +26 -0
  62. data/examples/v2/hooks/post_tool_use_multi_matcher_exit_1.yml +26 -0
  63. data/examples/v2/hooks/post_tool_use_multi_matcher_exit_2.yml +26 -0
  64. data/examples/v2/hooks/pre_tool_use_exit_0.yml +24 -0
  65. data/examples/v2/hooks/pre_tool_use_exit_1.yml +24 -0
  66. data/examples/v2/hooks/pre_tool_use_exit_2.yml +24 -0
  67. data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_0.yml +26 -0
  68. data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_1.yml +26 -0
  69. data/examples/v2/hooks/pre_tool_use_multi_matcher_exit_2.yml +27 -0
  70. data/examples/v2/hooks/swarm_summary.sh +44 -0
  71. data/examples/v2/hooks/user_prompt_exit_0.yml +21 -0
  72. data/examples/v2/hooks/user_prompt_exit_1.yml +21 -0
  73. data/examples/v2/hooks/user_prompt_exit_2.yml +21 -0
  74. data/examples/v2/hooks/validate_bash.rb +59 -0
  75. data/examples/v2/multi_directory_permissions.yml +221 -0
  76. data/examples/v2/node_context_demo.rb +127 -0
  77. data/examples/v2/node_workflow.rb +173 -0
  78. data/examples/v2/path_resolution_demo.rb +216 -0
  79. data/examples/v2/simple-swarm-v2.rb +90 -0
  80. data/examples/v2/simple-swarm-v2.yml +62 -0
  81. data/examples/v2/swarm.yml +71 -0
  82. data/examples/v2/swarm_with_hooks.yml +61 -0
  83. data/examples/v2/swarm_with_hooks_simple.yml +25 -0
  84. data/examples/v2/think_tool_demo.rb +62 -0
  85. data/exe/swarm +6 -0
  86. data/lib/claude_swarm/claude_mcp_server.rb +0 -6
  87. data/lib/claude_swarm/cli.rb +10 -3
  88. data/lib/claude_swarm/commands/ps.rb +19 -20
  89. data/lib/claude_swarm/commands/show.rb +1 -1
  90. data/lib/claude_swarm/configuration.rb +10 -12
  91. data/lib/claude_swarm/mcp_generator.rb +10 -1
  92. data/lib/claude_swarm/orchestrator.rb +73 -49
  93. data/lib/claude_swarm/system_utils.rb +37 -11
  94. data/lib/claude_swarm/version.rb +1 -1
  95. data/lib/claude_swarm/worktree_manager.rb +1 -0
  96. data/lib/claude_swarm/yaml_loader.rb +22 -0
  97. data/lib/claude_swarm.rb +6 -2
  98. data/lib/swarm_cli/cli.rb +201 -0
  99. data/lib/swarm_cli/command_registry.rb +61 -0
  100. data/lib/swarm_cli/commands/mcp_serve.rb +130 -0
  101. data/lib/swarm_cli/commands/mcp_tools.rb +148 -0
  102. data/lib/swarm_cli/commands/migrate.rb +55 -0
  103. data/lib/swarm_cli/commands/run.rb +173 -0
  104. data/lib/swarm_cli/config_loader.rb +97 -0
  105. data/lib/swarm_cli/formatters/human_formatter.rb +711 -0
  106. data/lib/swarm_cli/formatters/json_formatter.rb +51 -0
  107. data/lib/swarm_cli/interactive_repl.rb +918 -0
  108. data/lib/swarm_cli/mcp_serve_options.rb +44 -0
  109. data/lib/swarm_cli/mcp_tools_options.rb +59 -0
  110. data/lib/swarm_cli/migrate_options.rb +54 -0
  111. data/lib/swarm_cli/migrator.rb +132 -0
  112. data/lib/swarm_cli/options.rb +151 -0
  113. data/lib/swarm_cli/ui/components/agent_badge.rb +33 -0
  114. data/lib/swarm_cli/ui/components/content_block.rb +120 -0
  115. data/lib/swarm_cli/ui/components/divider.rb +57 -0
  116. data/lib/swarm_cli/ui/components/panel.rb +62 -0
  117. data/lib/swarm_cli/ui/components/usage_stats.rb +70 -0
  118. data/lib/swarm_cli/ui/formatters/cost.rb +49 -0
  119. data/lib/swarm_cli/ui/formatters/number.rb +58 -0
  120. data/lib/swarm_cli/ui/formatters/text.rb +77 -0
  121. data/lib/swarm_cli/ui/formatters/time.rb +73 -0
  122. data/lib/swarm_cli/ui/icons.rb +59 -0
  123. data/lib/swarm_cli/ui/renderers/event_renderer.rb +188 -0
  124. data/lib/swarm_cli/ui/state/agent_color_cache.rb +45 -0
  125. data/lib/swarm_cli/ui/state/depth_tracker.rb +40 -0
  126. data/lib/swarm_cli/ui/state/spinner_manager.rb +170 -0
  127. data/lib/swarm_cli/ui/state/usage_tracker.rb +62 -0
  128. data/lib/swarm_cli/version.rb +5 -0
  129. data/lib/swarm_cli.rb +44 -0
  130. data/lib/swarm_memory/adapters/base.rb +141 -0
  131. data/lib/swarm_memory/adapters/filesystem_adapter.rb +845 -0
  132. data/lib/swarm_memory/chat_extension.rb +34 -0
  133. data/lib/swarm_memory/cli/commands.rb +306 -0
  134. data/lib/swarm_memory/core/entry.rb +37 -0
  135. data/lib/swarm_memory/core/frontmatter_parser.rb +108 -0
  136. data/lib/swarm_memory/core/metadata_extractor.rb +68 -0
  137. data/lib/swarm_memory/core/path_normalizer.rb +75 -0
  138. data/lib/swarm_memory/core/semantic_index.rb +244 -0
  139. data/lib/swarm_memory/core/storage.rb +288 -0
  140. data/lib/swarm_memory/core/storage_read_tracker.rb +63 -0
  141. data/lib/swarm_memory/dsl/builder_extension.rb +40 -0
  142. data/lib/swarm_memory/dsl/memory_config.rb +113 -0
  143. data/lib/swarm_memory/embeddings/embedder.rb +36 -0
  144. data/lib/swarm_memory/embeddings/informers_embedder.rb +152 -0
  145. data/lib/swarm_memory/errors.rb +21 -0
  146. data/lib/swarm_memory/integration/cli_registration.rb +30 -0
  147. data/lib/swarm_memory/integration/configuration.rb +43 -0
  148. data/lib/swarm_memory/integration/registration.rb +31 -0
  149. data/lib/swarm_memory/integration/sdk_plugin.rb +531 -0
  150. data/lib/swarm_memory/optimization/analyzer.rb +244 -0
  151. data/lib/swarm_memory/optimization/defragmenter.rb +863 -0
  152. data/lib/swarm_memory/prompts/memory.md.erb +109 -0
  153. data/lib/swarm_memory/prompts/memory_assistant.md.erb +181 -0
  154. data/lib/swarm_memory/prompts/memory_researcher.md.erb +281 -0
  155. data/lib/swarm_memory/prompts/memory_retrieval.md.erb +78 -0
  156. data/lib/swarm_memory/search/semantic_search.rb +112 -0
  157. data/lib/swarm_memory/search/text_search.rb +42 -0
  158. data/lib/swarm_memory/search/text_similarity.rb +80 -0
  159. data/lib/swarm_memory/skills/meta/deep-learning.md +101 -0
  160. data/lib/swarm_memory/skills/meta/deep-learning.yml +14 -0
  161. data/lib/swarm_memory/tools/load_skill.rb +313 -0
  162. data/lib/swarm_memory/tools/memory_defrag.rb +382 -0
  163. data/lib/swarm_memory/tools/memory_delete.rb +99 -0
  164. data/lib/swarm_memory/tools/memory_edit.rb +185 -0
  165. data/lib/swarm_memory/tools/memory_glob.rb +160 -0
  166. data/lib/swarm_memory/tools/memory_grep.rb +247 -0
  167. data/lib/swarm_memory/tools/memory_multi_edit.rb +281 -0
  168. data/lib/swarm_memory/tools/memory_read.rb +123 -0
  169. data/lib/swarm_memory/tools/memory_write.rb +231 -0
  170. data/lib/swarm_memory/utils.rb +50 -0
  171. data/lib/swarm_memory/version.rb +5 -0
  172. data/lib/swarm_memory.rb +166 -0
  173. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +127 -0
  174. data/lib/swarm_sdk/agent/builder.rb +461 -0
  175. data/lib/swarm_sdk/agent/chat/context_tracker.rb +314 -0
  176. data/lib/swarm_sdk/agent/chat/hook_integration.rb +372 -0
  177. data/lib/swarm_sdk/agent/chat/logging_helpers.rb +116 -0
  178. data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +152 -0
  179. data/lib/swarm_sdk/agent/chat.rb +1159 -0
  180. data/lib/swarm_sdk/agent/context.rb +112 -0
  181. data/lib/swarm_sdk/agent/context_manager.rb +309 -0
  182. data/lib/swarm_sdk/agent/definition.rb +556 -0
  183. data/lib/swarm_sdk/claude_code_agent_adapter.rb +205 -0
  184. data/lib/swarm_sdk/configuration.rb +296 -0
  185. data/lib/swarm_sdk/context_compactor/metrics.rb +147 -0
  186. data/lib/swarm_sdk/context_compactor/token_counter.rb +106 -0
  187. data/lib/swarm_sdk/context_compactor.rb +340 -0
  188. data/lib/swarm_sdk/hooks/adapter.rb +359 -0
  189. data/lib/swarm_sdk/hooks/context.rb +197 -0
  190. data/lib/swarm_sdk/hooks/definition.rb +80 -0
  191. data/lib/swarm_sdk/hooks/error.rb +29 -0
  192. data/lib/swarm_sdk/hooks/executor.rb +146 -0
  193. data/lib/swarm_sdk/hooks/registry.rb +147 -0
  194. data/lib/swarm_sdk/hooks/result.rb +150 -0
  195. data/lib/swarm_sdk/hooks/shell_executor.rb +254 -0
  196. data/lib/swarm_sdk/hooks/tool_call.rb +35 -0
  197. data/lib/swarm_sdk/hooks/tool_result.rb +62 -0
  198. data/lib/swarm_sdk/log_collector.rb +51 -0
  199. data/lib/swarm_sdk/log_stream.rb +69 -0
  200. data/lib/swarm_sdk/markdown_parser.rb +75 -0
  201. data/lib/swarm_sdk/model_aliases.json +5 -0
  202. data/lib/swarm_sdk/models.json +1 -0
  203. data/lib/swarm_sdk/models.rb +120 -0
  204. data/lib/swarm_sdk/node/agent_config.rb +49 -0
  205. data/lib/swarm_sdk/node/builder.rb +439 -0
  206. data/lib/swarm_sdk/node/transformer_executor.rb +248 -0
  207. data/lib/swarm_sdk/node_context.rb +170 -0
  208. data/lib/swarm_sdk/node_orchestrator.rb +384 -0
  209. data/lib/swarm_sdk/permissions/config.rb +239 -0
  210. data/lib/swarm_sdk/permissions/error_formatter.rb +121 -0
  211. data/lib/swarm_sdk/permissions/path_matcher.rb +35 -0
  212. data/lib/swarm_sdk/permissions/validator.rb +173 -0
  213. data/lib/swarm_sdk/permissions_builder.rb +122 -0
  214. data/lib/swarm_sdk/plugin.rb +147 -0
  215. data/lib/swarm_sdk/plugin_registry.rb +101 -0
  216. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +243 -0
  217. data/lib/swarm_sdk/providers/openai_with_responses.rb +582 -0
  218. data/lib/swarm_sdk/result.rb +97 -0
  219. data/lib/swarm_sdk/swarm/agent_initializer.rb +334 -0
  220. data/lib/swarm_sdk/swarm/all_agents_builder.rb +140 -0
  221. data/lib/swarm_sdk/swarm/builder.rb +586 -0
  222. data/lib/swarm_sdk/swarm/mcp_configurator.rb +151 -0
  223. data/lib/swarm_sdk/swarm/tool_configurator.rb +419 -0
  224. data/lib/swarm_sdk/swarm.rb +982 -0
  225. data/lib/swarm_sdk/tools/bash.rb +274 -0
  226. data/lib/swarm_sdk/tools/clock.rb +44 -0
  227. data/lib/swarm_sdk/tools/delegate.rb +164 -0
  228. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +83 -0
  229. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +99 -0
  230. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +101 -0
  231. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +78 -0
  232. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +194 -0
  233. data/lib/swarm_sdk/tools/edit.rb +150 -0
  234. data/lib/swarm_sdk/tools/glob.rb +158 -0
  235. data/lib/swarm_sdk/tools/grep.rb +228 -0
  236. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +43 -0
  237. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +163 -0
  238. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +65 -0
  239. data/lib/swarm_sdk/tools/multi_edit.rb +232 -0
  240. data/lib/swarm_sdk/tools/path_resolver.rb +43 -0
  241. data/lib/swarm_sdk/tools/read.rb +251 -0
  242. data/lib/swarm_sdk/tools/registry.rb +93 -0
  243. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +96 -0
  244. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +76 -0
  245. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +91 -0
  246. data/lib/swarm_sdk/tools/stores/read_tracker.rb +61 -0
  247. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +224 -0
  248. data/lib/swarm_sdk/tools/stores/storage.rb +148 -0
  249. data/lib/swarm_sdk/tools/stores/todo_manager.rb +65 -0
  250. data/lib/swarm_sdk/tools/think.rb +95 -0
  251. data/lib/swarm_sdk/tools/todo_write.rb +216 -0
  252. data/lib/swarm_sdk/tools/web_fetch.rb +261 -0
  253. data/lib/swarm_sdk/tools/write.rb +117 -0
  254. data/lib/swarm_sdk/utils.rb +50 -0
  255. data/lib/swarm_sdk/version.rb +5 -0
  256. data/lib/swarm_sdk.rb +157 -0
  257. data/llm.v2.txt +13407 -0
  258. data/rubocop/cop/security/no_reflection_methods.rb +47 -0
  259. data/rubocop/cop/security/no_ruby_llm_logger.rb +32 -0
  260. data/swarm_cli.gemspec +57 -0
  261. data/swarm_memory.gemspec +28 -0
  262. data/swarm_sdk.gemspec +41 -0
  263. data/team.yml +1 -1
  264. data/team_full.yml +1875 -0
  265. data/{team_v2.yml → team_sdk.yml} +121 -52
  266. metadata +247 -4
  267. data/EXAMPLES.md +0 -164
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module Agent
5
+ # AgentContext encapsulates per-agent state and metadata
6
+ #
7
+ # Each agent has its own context that tracks:
8
+ # - Agent identity (name)
9
+ # - Delegation relationships (which tool calls are delegations)
10
+ # - Context window warnings (which thresholds have been hit)
11
+ # - Optional metadata
12
+ #
13
+ # This class replaces the per-agent hash maps that were previously
14
+ # stored in UnifiedLogger.
15
+ #
16
+ # @example
17
+ # context = Agent::Context.new(
18
+ # name: :backend,
19
+ # delegation_tools: ["DelegateToDatabase", "DelegateToAuth"],
20
+ # metadata: { role: "backend" }
21
+ # )
22
+ #
23
+ # # Track a delegation
24
+ # context.track_delegation(call_id: "call_123", target: "DelegateToDatabase")
25
+ #
26
+ # # Check if a tool call is a delegation
27
+ # context.delegation?(call_id: "call_123") # => true
28
+ class Context
29
+ # Thresholds for context limit warnings (in percentage)
30
+ # 60% triggers automatic compression, 80%/90% are informational warnings
31
+ CONTEXT_WARNING_THRESHOLDS = [60, 80, 90].freeze
32
+
33
+ # Threshold at which automatic compression is triggered
34
+ COMPRESSION_THRESHOLD = 60
35
+
36
+ attr_reader :name, :delegation_tools, :metadata, :warning_thresholds_hit
37
+
38
+ # Initialize a new agent context
39
+ #
40
+ # @param name [Symbol, String] Agent name
41
+ # @param delegation_tools [Array<String>] Names of tools that are delegations
42
+ # @param metadata [Hash] Optional metadata about the agent
43
+ def initialize(name:, delegation_tools: [], metadata: {})
44
+ @name = name.to_sym
45
+ @delegation_tools = Set.new(delegation_tools.map(&:to_s))
46
+ @metadata = metadata
47
+ @delegation_call_ids = Set.new
48
+ @delegation_targets = {}
49
+ @warning_thresholds_hit = Set.new
50
+ end
51
+
52
+ # Track a delegation tool call
53
+ #
54
+ # @param call_id [String] Tool call ID
55
+ # @param target [String] Target agent/tool name
56
+ # @return [void]
57
+ def track_delegation(call_id:, target:)
58
+ @delegation_call_ids.add(call_id)
59
+ @delegation_targets[call_id] = target
60
+ end
61
+
62
+ # Check if a tool call is a delegation
63
+ #
64
+ # @param call_id [String] Tool call ID
65
+ # @return [Boolean]
66
+ def delegation?(call_id:)
67
+ @delegation_call_ids.include?(call_id)
68
+ end
69
+
70
+ # Get the delegation target for a tool call
71
+ #
72
+ # @param call_id [String] Tool call ID
73
+ # @return [String, nil] Target agent/tool name, or nil if not a delegation
74
+ def delegation_target(call_id:)
75
+ @delegation_targets[call_id]
76
+ end
77
+
78
+ # Remove a delegation from tracking (after it completes)
79
+ #
80
+ # @param call_id [String] Tool call ID
81
+ # @return [void]
82
+ def clear_delegation(call_id:)
83
+ @delegation_targets.delete(call_id)
84
+ @delegation_call_ids.delete(call_id)
85
+ end
86
+
87
+ # Check if a tool name is a delegation tool
88
+ #
89
+ # @param tool_name [String] Tool name
90
+ # @return [Boolean]
91
+ def delegation_tool?(tool_name)
92
+ @delegation_tools.include?(tool_name.to_s)
93
+ end
94
+
95
+ # Record that a context warning threshold has been hit
96
+ #
97
+ # @param threshold [Integer] Threshold percentage (80, 90, etc)
98
+ # @return [Boolean] true if this is the first time hitting this threshold
99
+ def hit_warning_threshold?(threshold)
100
+ !@warning_thresholds_hit.add?(threshold).nil?
101
+ end
102
+
103
+ # Check if a warning threshold has been hit
104
+ #
105
+ # @param threshold [Integer] Threshold percentage
106
+ # @return [Boolean]
107
+ def warning_threshold_hit?(threshold)
108
+ @warning_thresholds_hit.include?(threshold)
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,309 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module Agent
5
+ # Manages conversation context and message optimization
6
+ #
7
+ # Responsibilities:
8
+ # - Handle ephemeral messages (sent to LLM but not persisted)
9
+ # - Extract and strip system reminders
10
+ # - Prepare messages for LLM API calls
11
+ # - Future: Context window management, summarization, truncation
12
+ #
13
+ # @example
14
+ # manager = ContextManager.new
15
+ # manager.add_ephemeral_reminder("<system-reminder>Use caution</system-reminder>")
16
+ # messages_for_llm = manager.prepare_for_llm(persistent_messages)
17
+ # manager.clear_ephemeral # After LLM call
18
+ class ContextManager
19
+ SYSTEM_REMINDER_REGEX = %r{<system-reminder>.*?</system-reminder>}m
20
+
21
+ def initialize
22
+ # Ephemeral content to append to messages for this turn only
23
+ # Format: { message_index => [array of reminder strings] }
24
+ @ephemeral_content = {}
25
+ end
26
+
27
+ # Track ephemeral content to append to a specific message
28
+ #
29
+ # Reminders will be embedded in the message content when sent to LLM,
30
+ # but are NOT persisted in the message history.
31
+ #
32
+ # @param message_index [Integer] Index of message to append to
33
+ # @param content [String] Reminder content to append
34
+ # @return [void]
35
+ def add_ephemeral_content_for_message(message_index, content)
36
+ @ephemeral_content[message_index] ||= []
37
+ @ephemeral_content[message_index] << content
38
+ end
39
+
40
+ # Add ephemeral reminder to the most recent message
41
+ #
42
+ # This will append the reminder to the last message in the array when
43
+ # preparing for LLM, but won't modify the stored message.
44
+ #
45
+ # @param content [String] Reminder content
46
+ # @param messages_array [Array<RubyLLM::Message>] Message array to get index from
47
+ # @return [void]
48
+ def add_ephemeral_reminder(content, messages_array:)
49
+ message_index = messages_array.size - 1
50
+ return if message_index < 0
51
+
52
+ add_ephemeral_content_for_message(message_index, content)
53
+ end
54
+
55
+ # Prepare messages for LLM API call
56
+ #
57
+ # Embeds ephemeral content into messages for this turn only.
58
+ # Does NOT modify the persistent messages array.
59
+ #
60
+ # @param persistent_messages [Array<RubyLLM::Message>] Messages from @messages
61
+ # @return [Array<RubyLLM::Message>] Messages with ephemeral content embedded
62
+ def prepare_for_llm(persistent_messages)
63
+ return persistent_messages.dup if @ephemeral_content.empty?
64
+
65
+ # Clone messages and embed ephemeral content
66
+ messages_for_llm = persistent_messages.map.with_index do |msg, index|
67
+ ephemeral_for_this_msg = @ephemeral_content[index]
68
+
69
+ # No ephemeral content for this message - use as-is
70
+ next msg unless ephemeral_for_this_msg&.any?
71
+
72
+ # Embed ephemeral content in this message
73
+ original_content = msg.content.is_a?(RubyLLM::Content) ? msg.content.text : msg.content.to_s
74
+ embedded_content = [original_content, *ephemeral_for_this_msg].join("\n\n")
75
+
76
+ # Create new message with embedded content
77
+ if msg.content.is_a?(RubyLLM::Content)
78
+ RubyLLM::Message.new(
79
+ role: msg.role,
80
+ content: RubyLLM::Content.new(embedded_content, msg.content.attachments),
81
+ tool_call_id: msg.tool_call_id,
82
+ )
83
+ else
84
+ RubyLLM::Message.new(
85
+ role: msg.role,
86
+ content: embedded_content,
87
+ tool_call_id: msg.tool_call_id,
88
+ )
89
+ end
90
+ end
91
+
92
+ messages_for_llm
93
+ end
94
+
95
+ # Clear all ephemeral content
96
+ #
97
+ # Should be called after LLM response is received.
98
+ #
99
+ # @return [void]
100
+ def clear_ephemeral
101
+ @ephemeral_content.clear
102
+ end
103
+
104
+ # Check if there is pending ephemeral content
105
+ #
106
+ # @return [Boolean] True if ephemeral content exists
107
+ def has_ephemeral?
108
+ @ephemeral_content.any?
109
+ end
110
+
111
+ # Get count of messages with ephemeral content
112
+ #
113
+ # @return [Integer] Number of messages with ephemeral content attached
114
+ def ephemeral_count
115
+ @ephemeral_content.size
116
+ end
117
+
118
+ # Extract all <system-reminder> blocks from content
119
+ #
120
+ # @param content [String] Content to extract from
121
+ # @return [Array<String>] Array of system reminder blocks
122
+ def extract_system_reminders(content)
123
+ return [] if content.nil? || content.empty?
124
+
125
+ content.scan(SYSTEM_REMINDER_REGEX)
126
+ end
127
+
128
+ # Strip all <system-reminder> blocks from content
129
+ #
130
+ # Returns clean content without system reminders.
131
+ #
132
+ # @param content [String] Content to strip from
133
+ # @return [String] Clean content
134
+ def strip_system_reminders(content)
135
+ return content if content.nil? || content.empty?
136
+
137
+ content.gsub(SYSTEM_REMINDER_REGEX, "").strip
138
+ end
139
+
140
+ # Check if content contains system reminders
141
+ #
142
+ # @param content [String] Content to check
143
+ # @return [Boolean] True if reminders found
144
+ def has_system_reminders?(content)
145
+ return false if content.nil? || content.empty?
146
+
147
+ SYSTEM_REMINDER_REGEX.match?(content)
148
+ end
149
+
150
+ # ============================================================================
151
+ # FUTURE: Context Optimization Methods (Hooks for Later Implementation)
152
+ # ============================================================================
153
+
154
+ # Future: Summarize old messages to save context window space
155
+ #
156
+ # @param messages [Array<RubyLLM::Message>] Messages to potentially summarize
157
+ # @param before_index [Integer] Summarize messages before this index
158
+ # @param strategy [Symbol] Summarization strategy (:llm, :truncate, :remove)
159
+ # @return [Array<RubyLLM::Message>] Optimized message array
160
+ def summarize_old_messages(messages, before_index:, strategy: :truncate)
161
+ # TODO: Implement when needed
162
+ messages
163
+ end
164
+
165
+ # Future: Truncate messages to fit within context window
166
+ #
167
+ # @param messages [Array<RubyLLM::Message>] Messages to fit
168
+ # @param max_tokens [Integer] Maximum token budget
169
+ # @param keep_recent [Integer] Number of recent messages to always keep
170
+ # @return [Array<RubyLLM::Message>] Truncated messages
171
+ def truncate_to_fit(messages, max_tokens:, keep_recent: 10)
172
+ # TODO: Implement when needed
173
+ messages
174
+ end
175
+
176
+ # Compress verbose tool results for older messages
177
+ #
178
+ # Uses progressive compression: older messages are compressed more aggressively.
179
+ # Preserves user/assistant messages at full detail (conversational context).
180
+ #
181
+ # @param messages [Array<RubyLLM::Message>] Messages to compress
182
+ # @param keep_recent [Integer] Number of recent messages to keep at full detail
183
+ # @return [Array<RubyLLM::Message>] Compressed messages
184
+ def compress_tool_results(messages, keep_recent: 10)
185
+ messages.map.with_index do |msg, i|
186
+ # Keep recent messages at full detail
187
+ next msg if i >= messages.size - keep_recent
188
+
189
+ # Keep user/assistant messages (conversational flow is important)
190
+ next msg if [:user, :assistant].include?(msg.role)
191
+
192
+ # Compress old tool results
193
+ if msg.role == :tool
194
+ compress_tool_message(msg, age: messages.size - i)
195
+ else
196
+ msg
197
+ end
198
+ end
199
+ end
200
+
201
+ # Compress a single tool message based on age
202
+ #
203
+ # Progressive compression: older messages get compressed more.
204
+ # For re-runnable tools (Read, Grep, Glob, etc.), adds instruction to re-run if needed.
205
+ #
206
+ # @param msg [RubyLLM::Message] Tool message to compress
207
+ # @param age [Integer] How many messages ago (higher = older)
208
+ # @return [RubyLLM::Message] Compressed message
209
+ def compress_tool_message(msg, age:)
210
+ content = msg.content.to_s
211
+
212
+ # Progressive compression based on age
213
+ max_length = case age
214
+ when 0..10 then return msg # Recent: keep full detail
215
+ when 11..20 then 1000 # Medium age: light compression
216
+ when 21..40 then 500 # Old: moderate compression
217
+ when 41..60 then 200 # Very old: heavy compression
218
+ else 100 # Ancient: minimal summary
219
+ end
220
+
221
+ return msg if content.length <= max_length
222
+
223
+ # Compress while preserving structure
224
+ compressed = content.slice(0, max_length)
225
+ truncated_chars = content.length - max_length
226
+ compressed += "\n...[#{truncated_chars} chars truncated for context management]"
227
+
228
+ # Detect if this is a re-runnable tool and add helpful instruction
229
+ tool_name = detect_tool_name(content)
230
+ if rerunnable_tool?(tool_name)
231
+ compressed += "\n\nšŸ’” If you need the full output, re-run the #{tool_name} tool with the same parameters."
232
+ end
233
+
234
+ RubyLLM::Message.new(
235
+ role: :tool,
236
+ content: compressed,
237
+ tool_call_id: msg.tool_call_id,
238
+ )
239
+ end
240
+
241
+ # Detect tool name from content
242
+ #
243
+ # @param content [String] Tool result content
244
+ # @return [String, nil] Tool name or nil
245
+ def detect_tool_name(content)
246
+ # Many tool results start with patterns we can detect
247
+ case content
248
+ when /^\s*\d+→/ # Line numbers (Read, MemoryRead)
249
+ content.include?("memory://") ? "MemoryRead" : "Read"
250
+ when /^Memory entries matching/ # MemoryGlob
251
+ "MemoryGlob"
252
+ when /^Found \d+ files? matching/ # Glob
253
+ "Glob"
254
+ when /matches in \d+ files?|No matches found/ # Grep, MemoryGrep
255
+ content.include?("memory://") ? "MemoryGrep" : "Grep"
256
+ when %r{^Stored at memory://} # MemoryWrite (not re-runnable but identifiable)
257
+ "MemoryWrite"
258
+ when %r{^Deleted memory://} # MemoryDelete
259
+ "MemoryDelete"
260
+ end
261
+ end
262
+
263
+ # Check if a tool is re-runnable (idempotent, can get same data again)
264
+ #
265
+ # @param tool_name [String, nil] Tool name
266
+ # @return [Boolean] True if tool can be re-run safely
267
+ def rerunnable_tool?(tool_name)
268
+ return false if tool_name.nil?
269
+
270
+ # These tools are idempotent - re-running gives same/current data
271
+ ["Read", "MemoryRead", "Grep", "MemoryGrep", "Glob", "MemoryGlob"].include?(tool_name)
272
+ end
273
+
274
+ # Automatically compress messages when context threshold is hit
275
+ #
276
+ # This is called automatically when context usage crosses 60% threshold.
277
+ # Returns compressed messages array for immediate use.
278
+ #
279
+ # @param messages [Array<RubyLLM::Message>] Current message array
280
+ # @param keep_recent [Integer] Number of recent messages to keep full
281
+ # @return [Array<RubyLLM::Message>] Compressed messages
282
+ def auto_compress_on_threshold(messages, keep_recent: 10)
283
+ return messages if @compression_applied
284
+
285
+ # Mark as applied to avoid compressing multiple times
286
+ @compression_applied = true
287
+
288
+ compress_tool_results(messages, keep_recent: keep_recent)
289
+ end
290
+
291
+ # Reset compression flag (when conversation is reset)
292
+ #
293
+ # @return [void]
294
+ def reset_compression
295
+ @compression_applied = false
296
+ end
297
+
298
+ # Future: Detect if context is becoming bloated
299
+ #
300
+ # @param messages [Array<RubyLLM::Message>] Messages to analyze
301
+ # @param threshold [Float] Bloat threshold (0.0-1.0)
302
+ # @return [Hash] Bloat analysis with recommendations
303
+ def analyze_context_bloat(messages, threshold: 0.7)
304
+ # TODO: Implement when needed
305
+ { bloated: false, recommendations: [] }
306
+ end
307
+ end
308
+ end
309
+ end