claude_swarm 1.0.0 → 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 +21 -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 +7 -3
  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,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module Agent
5
+ class Chat < RubyLLM::Chat
6
+ # Helper methods for logging and serialization of tool calls and results
7
+ #
8
+ # Responsibilities:
9
+ # - Format tool calls for logging
10
+ # - Serialize tool results (handling different types)
11
+ # - Calculate LLM costs based on token usage
12
+ #
13
+ # These are stateless utility methods that operate on data structures.
14
+ module LoggingHelpers
15
+ # Format tool calls for logging
16
+ #
17
+ # @param tool_calls_hash [Hash] Tool calls from message
18
+ # @return [Array<Hash>, nil] Formatted tool calls
19
+ def format_tool_calls(tool_calls_hash)
20
+ return unless tool_calls_hash
21
+
22
+ tool_calls_hash.map do |_id, tc|
23
+ {
24
+ id: tc.id,
25
+ name: tc.name,
26
+ arguments: tc.arguments,
27
+ }
28
+ end
29
+ end
30
+
31
+ # Serialize a tool result for logging
32
+ #
33
+ # Handles multiple result types:
34
+ # - String: pass through
35
+ # - Hash/Array: pass through
36
+ # - RubyLLM::Content: extract text and attachment info
37
+ # - Other: convert to string
38
+ #
39
+ # @param result [String, Hash, Array, RubyLLM::Content, Object] Tool result
40
+ # @return [String, Hash, Array] Serialized result
41
+ def serialize_result(result)
42
+ case result
43
+ when String then result
44
+ when Hash, Array then result
45
+ when RubyLLM::Content
46
+ # Format Content objects to show text and attachment info
47
+ parts = []
48
+ parts << result.text if result.text && !result.text.empty?
49
+
50
+ if result.attachments.any?
51
+ attachment_info = result.attachments.map do |att|
52
+ "#{att.source} (#{att.mime_type})"
53
+ end.join(", ")
54
+ parts << "[Attachments: #{attachment_info}]"
55
+ end
56
+
57
+ parts.join(" ")
58
+ else
59
+ result.to_s
60
+ end
61
+ end
62
+
63
+ # Calculate LLM cost for a message
64
+ #
65
+ # Uses RubyLLM's model registry to get pricing information.
66
+ # Returns zero cost if pricing is unavailable.
67
+ #
68
+ # @param message [RubyLLM::Message] Message with token counts
69
+ # @return [Hash] Cost breakdown { input_cost:, output_cost:, total_cost: }
70
+ def calculate_cost(message)
71
+ return zero_cost unless message.input_tokens && message.output_tokens
72
+
73
+ # Use SwarmSDK's model registry (not RubyLLM's) for up-to-date pricing
74
+ model_info = SwarmSDK::Models.find(message.model_id)
75
+ return zero_cost unless model_info
76
+
77
+ # Extract pricing from SwarmSDK's models.json structure
78
+ pricing = model_info["pricing"] || model_info[:pricing]
79
+ return zero_cost unless pricing
80
+
81
+ text_pricing = pricing["text_tokens"] || pricing[:text_tokens]
82
+ return zero_cost unless text_pricing
83
+
84
+ standard_pricing = text_pricing["standard"] || text_pricing[:standard]
85
+ return zero_cost unless standard_pricing
86
+
87
+ input_price = standard_pricing["input_per_million"] || standard_pricing[:input_per_million]
88
+ output_price = standard_pricing["output_per_million"] || standard_pricing[:output_per_million]
89
+
90
+ return zero_cost unless input_price && output_price
91
+
92
+ # Calculate costs (prices are per million tokens in USD)
93
+ input_cost = (message.input_tokens / 1_000_000.0) * input_price
94
+ output_cost = (message.output_tokens / 1_000_000.0) * output_price
95
+
96
+ {
97
+ input_cost: input_cost,
98
+ output_cost: output_cost,
99
+ total_cost: input_cost + output_cost,
100
+ }
101
+ rescue StandardError => e
102
+ # Model not found in registry or pricing not available
103
+ RubyLLM.logger.debug("Cost calculation failed for #{message.model_id}: #{e.message}")
104
+ zero_cost
105
+ end
106
+
107
+ # Zero cost fallback
108
+ #
109
+ # @return [Hash] Zero cost breakdown
110
+ def zero_cost
111
+ { input_cost: 0.0, output_cost: 0.0, total_cost: 0.0 }
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module Agent
5
+ class Chat < RubyLLM::Chat
6
+ # Handles injection of system reminders at strategic points in the conversation
7
+ #
8
+ # Responsibilities:
9
+ # - Inject reminders before/after first user message
10
+ # - Inject periodic TodoWrite reminders
11
+ # - Track when reminders were last injected
12
+ #
13
+ # This class is stateless - it operates on the chat's message history.
14
+ class SystemReminderInjector
15
+ # System reminder to inject BEFORE the first user message
16
+ BEFORE_FIRST_MESSAGE_REMINDER = <<~REMINDER.strip
17
+ <system-reminder>
18
+ As you answer the user's questions, you can use the following context:
19
+
20
+ # important-instruction-reminders
21
+
22
+ Do what has been asked; nothing more, nothing less.
23
+ NEVER create files unless they're absolutely necessary for achieving your goal.
24
+ ALWAYS prefer editing an existing file to creating a new one.
25
+ NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.
26
+
27
+ IMPORTANT: this context may or may not be relevant to your tasks. You should not respond to this context unless it is highly relevant to your task.
28
+
29
+ </system-reminder>
30
+ REMINDER
31
+
32
+ # System reminder to inject AFTER the first user message
33
+ AFTER_FIRST_MESSAGE_REMINDER = <<~REMINDER.strip
34
+ <system-reminder>Your todo list is currently empty. DO NOT mention this to the user. If this task requires multiple steps: (1) FIRST analyze the scope by searching/reading files, (2) SECOND create a COMPLETE todo list with ALL tasks before starting work, (3) THIRD execute tasks one by one. Only skip the todo list for simple single-step tasks. Do not mention this message to the user.</system-reminder>
35
+ REMINDER
36
+
37
+ # Periodic reminder about TodoWrite tool usage
38
+ TODOWRITE_PERIODIC_REMINDER = <<~REMINDER.strip
39
+ <system-reminder>The TodoWrite tool hasn't been used recently. If you're working on tasks that would benefit from tracking progress, consider using the TodoWrite tool to track progress. Also consider cleaning up the todo list if has become stale and no longer matches what you are working on. Only use it if it's relevant to the current work. This is just a gentle reminder - ignore if not applicable.</system-reminder>
40
+ REMINDER
41
+
42
+ # Number of messages between TodoWrite reminders
43
+ TODOWRITE_REMINDER_INTERVAL = 8
44
+
45
+ class << self
46
+ # Check if this is the first user message in the conversation
47
+ #
48
+ # @param chat [Agent::Chat] The chat instance
49
+ # @return [Boolean] true if no user messages exist yet
50
+ def first_message?(chat)
51
+ chat.messages.none? { |msg| msg.role == :user }
52
+ end
53
+
54
+ # Inject first message reminders (before + after user message)
55
+ #
56
+ # This manually constructs the first message sequence with system reminders
57
+ # sandwiching the actual user prompt.
58
+ #
59
+ # Sequence:
60
+ # 1. BEFORE_FIRST_MESSAGE_REMINDER (general reminders)
61
+ # 2. Toolset reminder (list of available tools)
62
+ # 3. User's actual prompt
63
+ # 4. AFTER_FIRST_MESSAGE_REMINDER (todo list reminder)
64
+ #
65
+ # @param chat [Agent::Chat] The chat instance
66
+ # @param prompt [String] The user's actual prompt
67
+ # @return [void]
68
+ def inject_first_message_reminders(chat, prompt)
69
+ # Build user message with embedded reminders
70
+ # Reminders are embedded in the content, not separate messages
71
+ full_content = [
72
+ prompt,
73
+ BEFORE_FIRST_MESSAGE_REMINDER,
74
+ build_toolset_reminder(chat),
75
+ AFTER_FIRST_MESSAGE_REMINDER,
76
+ ].join("\n\n")
77
+
78
+ # Extract reminders and add clean prompt to persistent history
79
+ reminders = chat.context_manager.extract_system_reminders(full_content)
80
+ clean_prompt = chat.context_manager.strip_system_reminders(full_content)
81
+
82
+ # Store clean prompt (without reminders) in conversation history
83
+ chat.add_message(role: :user, content: clean_prompt)
84
+
85
+ # Track reminders to embed in this message when sending to LLM
86
+ reminders.each do |reminder|
87
+ chat.context_manager.add_ephemeral_reminder(reminder, messages_array: chat.messages)
88
+ end
89
+ end
90
+
91
+ # Build toolset reminder listing all available tools
92
+ #
93
+ # @param chat [Agent::Chat] The chat instance
94
+ # @return [String] System reminder with tool list
95
+ def build_toolset_reminder(chat)
96
+ tools_list = chat.tools.values.map(&:name).sort
97
+
98
+ reminder = "<system-reminder>\n"
99
+ reminder += "Tools available: #{tools_list.join(", ")}\n\n"
100
+ reminder += "Only use tools from this list. Do not attempt to use tools that are not listed here.\n"
101
+ reminder += "</system-reminder>"
102
+
103
+ reminder
104
+ end
105
+
106
+ # Check if we should inject a periodic TodoWrite reminder
107
+ #
108
+ # Injects a reminder if:
109
+ # 1. Enough messages have passed (>= 5)
110
+ # 2. TodoWrite hasn't been used in the last TODOWRITE_REMINDER_INTERVAL messages
111
+ #
112
+ # @param chat [Agent::Chat] The chat instance
113
+ # @param last_todowrite_index [Integer, nil] Index of last TodoWrite usage
114
+ # @return [Boolean] true if reminder should be injected
115
+ def should_inject_todowrite_reminder?(chat, last_todowrite_index)
116
+ # Need at least a few messages before reminding
117
+ return false if chat.messages.count < 5
118
+
119
+ # Find the last message that contains TodoWrite tool usage
120
+ last_todo_index = chat.messages.rindex do |msg|
121
+ msg.role == :tool && msg.content.to_s.include?("TodoWrite")
122
+ end
123
+
124
+ # Check if enough messages have passed since last TodoWrite
125
+ if last_todo_index.nil? && last_todowrite_index.nil?
126
+ # Never used TodoWrite - check if we've exceeded interval
127
+ chat.messages.count >= TODOWRITE_REMINDER_INTERVAL
128
+ elsif last_todo_index
129
+ # Recently used - don't remind
130
+ false
131
+ elsif last_todowrite_index
132
+ # Used before - check if interval has passed
133
+ chat.messages.count - last_todowrite_index >= TODOWRITE_REMINDER_INTERVAL
134
+ else
135
+ false
136
+ end
137
+ end
138
+
139
+ # Update the last TodoWrite index by finding it in messages
140
+ #
141
+ # @param chat [Agent::Chat] The chat instance
142
+ # @return [Integer, nil] Index of last TodoWrite usage, or nil
143
+ def find_last_todowrite_index(chat)
144
+ chat.messages.rindex do |msg|
145
+ msg.role == :tool && msg.content.to_s.include?("TodoWrite")
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end