swarm_sdk 2.0.0.pre.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 (68) hide show
  1. checksums.yaml +7 -0
  2. data/lib/swarm_sdk/agent/builder.rb +333 -0
  3. data/lib/swarm_sdk/agent/chat/context_tracker.rb +271 -0
  4. data/lib/swarm_sdk/agent/chat/hook_integration.rb +372 -0
  5. data/lib/swarm_sdk/agent/chat/logging_helpers.rb +99 -0
  6. data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +114 -0
  7. data/lib/swarm_sdk/agent/chat.rb +779 -0
  8. data/lib/swarm_sdk/agent/context.rb +108 -0
  9. data/lib/swarm_sdk/agent/definition.rb +335 -0
  10. data/lib/swarm_sdk/configuration.rb +251 -0
  11. data/lib/swarm_sdk/context_compactor/metrics.rb +147 -0
  12. data/lib/swarm_sdk/context_compactor/token_counter.rb +106 -0
  13. data/lib/swarm_sdk/context_compactor.rb +340 -0
  14. data/lib/swarm_sdk/hooks/adapter.rb +359 -0
  15. data/lib/swarm_sdk/hooks/context.rb +163 -0
  16. data/lib/swarm_sdk/hooks/definition.rb +80 -0
  17. data/lib/swarm_sdk/hooks/error.rb +29 -0
  18. data/lib/swarm_sdk/hooks/executor.rb +146 -0
  19. data/lib/swarm_sdk/hooks/registry.rb +143 -0
  20. data/lib/swarm_sdk/hooks/result.rb +150 -0
  21. data/lib/swarm_sdk/hooks/shell_executor.rb +254 -0
  22. data/lib/swarm_sdk/hooks/tool_call.rb +35 -0
  23. data/lib/swarm_sdk/hooks/tool_result.rb +62 -0
  24. data/lib/swarm_sdk/log_collector.rb +83 -0
  25. data/lib/swarm_sdk/log_stream.rb +69 -0
  26. data/lib/swarm_sdk/markdown_parser.rb +46 -0
  27. data/lib/swarm_sdk/permissions/config.rb +239 -0
  28. data/lib/swarm_sdk/permissions/error_formatter.rb +121 -0
  29. data/lib/swarm_sdk/permissions/path_matcher.rb +35 -0
  30. data/lib/swarm_sdk/permissions/validator.rb +173 -0
  31. data/lib/swarm_sdk/permissions_builder.rb +122 -0
  32. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +237 -0
  33. data/lib/swarm_sdk/providers/openai_with_responses.rb +582 -0
  34. data/lib/swarm_sdk/result.rb +97 -0
  35. data/lib/swarm_sdk/swarm/agent_initializer.rb +224 -0
  36. data/lib/swarm_sdk/swarm/all_agents_builder.rb +62 -0
  37. data/lib/swarm_sdk/swarm/builder.rb +240 -0
  38. data/lib/swarm_sdk/swarm/mcp_configurator.rb +151 -0
  39. data/lib/swarm_sdk/swarm/tool_configurator.rb +267 -0
  40. data/lib/swarm_sdk/swarm.rb +837 -0
  41. data/lib/swarm_sdk/tools/bash.rb +274 -0
  42. data/lib/swarm_sdk/tools/delegate.rb +152 -0
  43. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +83 -0
  44. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +99 -0
  45. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +78 -0
  46. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +194 -0
  47. data/lib/swarm_sdk/tools/edit.rb +150 -0
  48. data/lib/swarm_sdk/tools/glob.rb +158 -0
  49. data/lib/swarm_sdk/tools/grep.rb +231 -0
  50. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +43 -0
  51. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +163 -0
  52. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +65 -0
  53. data/lib/swarm_sdk/tools/multi_edit.rb +232 -0
  54. data/lib/swarm_sdk/tools/path_resolver.rb +43 -0
  55. data/lib/swarm_sdk/tools/read.rb +251 -0
  56. data/lib/swarm_sdk/tools/registry.rb +73 -0
  57. data/lib/swarm_sdk/tools/scratchpad_list.rb +88 -0
  58. data/lib/swarm_sdk/tools/scratchpad_read.rb +59 -0
  59. data/lib/swarm_sdk/tools/scratchpad_write.rb +88 -0
  60. data/lib/swarm_sdk/tools/stores/read_tracker.rb +61 -0
  61. data/lib/swarm_sdk/tools/stores/scratchpad.rb +153 -0
  62. data/lib/swarm_sdk/tools/stores/todo_manager.rb +65 -0
  63. data/lib/swarm_sdk/tools/todo_write.rb +216 -0
  64. data/lib/swarm_sdk/tools/write.rb +117 -0
  65. data/lib/swarm_sdk/utils.rb +50 -0
  66. data/lib/swarm_sdk/version.rb +5 -0
  67. data/lib/swarm_sdk.rb +69 -0
  68. metadata +169 -0
@@ -0,0 +1,372 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module Agent
5
+ class Chat < RubyLLM::Chat
6
+ # Integrates SwarmSDK's hook system with Agent::Chat
7
+ #
8
+ # Responsibilities:
9
+ # - Setup hook system (registry, executor, agent hooks)
10
+ # - Provide trigger methods for all hook events
11
+ # - Wrap ask() to inject user_prompt hooks
12
+ # - Handle hook results (halt, replace, continue, reprompt)
13
+ #
14
+ # This module is included in Agent::Chat and provides methods for triggering hooks.
15
+ # It overrides ask() to inject user_prompt hooks, but does NOT override
16
+ # handle_tool_calls (that's handled in Agent::Chat with explicit hook calls).
17
+ module HookIntegration
18
+ # Expose hook system components for ContextTracker
19
+ attr_reader :hook_executor, :hook_swarm, :hook_agent_hooks
20
+
21
+ # Setup the hook system for this agent chat
22
+ #
23
+ # This must be called after setup_context and before the first ask/complete.
24
+ # It wires up the hook system to trigger at the right times.
25
+ #
26
+ # @param registry [Hooks::Registry] Shared registry for named hooks and swarm defaults
27
+ # @param agent_definition [Agent::Definition] Agent configuration with hooks
28
+ # @param swarm [Swarm, nil] Reference to swarm for context
29
+ # @return [void]
30
+ def setup_hooks(registry:, agent_definition:, swarm: nil)
31
+ @hook_registry = registry
32
+ @hook_swarm = swarm
33
+ @hook_executor = Hooks::Executor.new(registry, logger: RubyLLM.logger)
34
+
35
+ # Extract agent hooks based on format
36
+ hooks = agent_definition.hooks || {}
37
+
38
+ # Check if hooks are pre-parsed HookDefinition objects (from DSL)
39
+ # or raw YAML hash (to be processed by Hooks::Adapter in pass_5)
40
+ @hook_agent_hooks = if hooks.is_a?(Hash) && hooks.values.all? { |v| v.is_a?(Array) && v.all? { |item| item.is_a?(Hooks::Definition) } }
41
+ # DSL hooks - already parsed, use them
42
+ hooks
43
+ else
44
+ # YAML hooks - raw hash, will be processed in pass_5 by Hooks::Adapter
45
+ # For now, use empty hash (pass_5 will add them later)
46
+ {}
47
+ end
48
+ end
49
+
50
+ # Add a hook programmatically at runtime
51
+ #
52
+ # This allows agents to add hooks dynamically, which is useful for
53
+ # implementing adaptive behavior or runtime monitoring.
54
+ #
55
+ # @param event [Symbol] Event type (e.g., :pre_tool_use)
56
+ # @param matcher [String, Regexp, nil] Optional regex pattern for tool names
57
+ # @param priority [Integer] Execution priority (higher = earlier)
58
+ # @param block [Proc] Hook implementation
59
+ def add_hook(event, matcher: nil, priority: 0, &block)
60
+ raise ArgumentError, "Hooks not set up. Call setup_hooks first." unless @hook_executor
61
+
62
+ definition = Hooks::Definition.new(
63
+ event: event,
64
+ matcher: matcher,
65
+ priority: priority,
66
+ proc: block,
67
+ )
68
+
69
+ @hook_agent_hooks[event] ||= []
70
+ @hook_agent_hooks[event] << definition
71
+ @hook_agent_hooks[event].sort_by! { |cb| -cb.priority }
72
+ end
73
+
74
+ # Override ask to trigger user_prompt hooks
75
+ #
76
+ # This wraps the Agent::Chat#ask implementation to inject hooks AFTER
77
+ # system reminders are handled.
78
+ #
79
+ # @param prompt [String] User prompt
80
+ # @param options [Hash] Additional options
81
+ # @return [RubyLLM::Message] LLM response
82
+ def ask(prompt, **options)
83
+ # Trigger user_prompt hook before sending to LLM (can halt or modify prompt)
84
+ if @hook_executor
85
+ hook_result = trigger_user_prompt(prompt)
86
+
87
+ # Check if hook halted execution
88
+ if hook_result[:halted]
89
+ # Return a halted message instead of calling LLM
90
+ return RubyLLM::Message.new(
91
+ role: :assistant,
92
+ content: hook_result[:halt_message],
93
+ model_id: model.id,
94
+ )
95
+ end
96
+
97
+ # Use modified prompt if hook provided one (stdout injection)
98
+ prompt = hook_result[:modified_prompt] if hook_result[:modified_prompt]
99
+ end
100
+
101
+ # Call original ask implementation (Agent::Chat handles system reminders)
102
+ super(prompt, **options)
103
+ end
104
+
105
+ # Override check_context_warnings to trigger our hook system
106
+ #
107
+ # This wraps the default context warning behavior to also trigger hooks.
108
+ def check_context_warnings
109
+ return unless respond_to?(:context_usage_percentage)
110
+
111
+ current_percentage = context_usage_percentage
112
+
113
+ Context::CONTEXT_WARNING_THRESHOLDS.each do |threshold|
114
+ # Only warn once per threshold
115
+ next if @agent_context.warning_threshold_hit?(threshold)
116
+ next if current_percentage < threshold
117
+
118
+ # Mark threshold as hit
119
+ @agent_context.hit_warning_threshold?(threshold)
120
+
121
+ # Emit existing log event (for backward compatibility)
122
+ LogStream.emit(
123
+ type: "context_limit_warning",
124
+ agent: @agent_context.name,
125
+ model: model.id,
126
+ threshold: "#{threshold}%",
127
+ current_usage: "#{current_percentage}%",
128
+ tokens_used: cumulative_total_tokens,
129
+ tokens_remaining: tokens_remaining,
130
+ context_limit: context_limit,
131
+ metadata: @agent_context.metadata,
132
+ )
133
+
134
+ # Trigger hook system
135
+ trigger_context_warning(threshold, current_percentage) if @hook_executor
136
+ end
137
+ end
138
+
139
+ # Trigger pre_tool_use hooks
140
+ #
141
+ # Should be called by Agent::Chat before tool execution.
142
+ # Returns a hash indicating whether to proceed and any custom result.
143
+ #
144
+ # @param tool_call [RubyLLM::ToolCall] Tool call from LLM
145
+ # @return [Hash] { proceed: true/false, custom_result: result (if any) }
146
+ def trigger_pre_tool_use(tool_call)
147
+ return { proceed: true } unless @hook_executor
148
+
149
+ context = build_hook_context(
150
+ event: :pre_tool_use,
151
+ tool_call: wrap_tool_call_to_hooks(tool_call),
152
+ )
153
+
154
+ agent_hooks = @hook_agent_hooks[:pre_tool_use] || []
155
+
156
+ result = @hook_executor.execute_safe(
157
+ event: :pre_tool_use,
158
+ context: context,
159
+ callbacks: agent_hooks,
160
+ )
161
+
162
+ # Return custom result if hook provides one
163
+ if result.replace?
164
+ { proceed: false, custom_result: result.value }
165
+ elsif result.halt?
166
+ { proceed: false, custom_result: result.value || "Tool execution blocked by hook" }
167
+ elsif result.finish_agent?
168
+ # Finish agent execution immediately with this message
169
+ { proceed: false, finish_agent: true, custom_result: result.value }
170
+ elsif result.finish_swarm?
171
+ # Finish entire swarm execution immediately with this message
172
+ { proceed: false, finish_swarm: true, custom_result: result.value }
173
+ else
174
+ { proceed: true }
175
+ end
176
+ end
177
+
178
+ # Trigger post_tool_use hooks
179
+ #
180
+ # Should be called by Agent::Chat after tool execution.
181
+ # Returns modified result if hook replaces it, or a special marker for finish actions.
182
+ #
183
+ # @param result [String, Object] Tool execution result
184
+ # @param tool_call [RubyLLM::ToolCall] Tool call object with full context
185
+ # @return [Object, Hash] Modified result if hook replaces it, hash with :finish_agent or :finish_swarm if finishing, otherwise original result
186
+ def trigger_post_tool_use(result, tool_call:)
187
+ return result unless @hook_executor
188
+
189
+ context = build_hook_context(
190
+ event: :post_tool_use,
191
+ tool_result: wrap_tool_result(tool_call.id, tool_call.name, result),
192
+ )
193
+
194
+ agent_hooks = @hook_agent_hooks[:post_tool_use] || []
195
+
196
+ hook_result = @hook_executor.execute_safe(
197
+ event: :post_tool_use,
198
+ context: context,
199
+ callbacks: agent_hooks,
200
+ )
201
+
202
+ # Return modified result or finish markers
203
+ if hook_result.replace?
204
+ hook_result.value
205
+ elsif hook_result.finish_agent?
206
+ { __finish_agent__: true, message: hook_result.value }
207
+ elsif hook_result.finish_swarm?
208
+ { __finish_swarm__: true, message: hook_result.value }
209
+ else
210
+ result
211
+ end
212
+ end
213
+
214
+ private
215
+
216
+ # Trigger context_warning hooks
217
+ #
218
+ # Hooks have access to the chat instance via metadata[:chat]
219
+ # to access and manipulate the messages array.
220
+ #
221
+ # @param threshold [Integer] Warning threshold percentage
222
+ # @param current_usage [Float] Current usage percentage
223
+ # @return [void]
224
+ def trigger_context_warning(threshold, current_usage)
225
+ return unless @hook_executor
226
+
227
+ context = build_hook_context(
228
+ event: :context_warning,
229
+ metadata: {
230
+ chat: self, # Provide access to chat instance (for messages array)
231
+ threshold: threshold,
232
+ percentage: current_usage,
233
+ tokens_used: cumulative_total_tokens,
234
+ tokens_remaining: tokens_remaining,
235
+ context_limit: context_limit,
236
+ },
237
+ )
238
+
239
+ agent_hooks = @hook_agent_hooks[:context_warning] || []
240
+
241
+ @hook_executor.execute_safe(
242
+ event: :context_warning,
243
+ context: context,
244
+ callbacks: agent_hooks,
245
+ )
246
+ end
247
+
248
+ # Trigger user_prompt hooks
249
+ #
250
+ # This fires before sending a user message to the LLM.
251
+ # Can halt execution or append hook stdout to prompt.
252
+ #
253
+ # @param prompt [String] User's message/prompt
254
+ # @return [Hash] { halted: bool, halt_message: String, modified_prompt: String }
255
+ def trigger_user_prompt(prompt)
256
+ return { halted: false, modified_prompt: prompt } unless @hook_executor
257
+
258
+ # Filter out delegation tools from tools list
259
+ actual_tools = if respond_to?(:tools) && @agent_context
260
+ tools.keys.reject { |tool_name| @agent_context.delegation_tool?(tool_name.to_s) }
261
+ else
262
+ []
263
+ end
264
+
265
+ # Extract agent names from delegation tool names
266
+ delegate_agents = if @agent_context&.delegation_tools
267
+ @agent_context.delegation_tools.map { |tool_name| @context_tracker.extract_delegate_agent_name(tool_name) }
268
+ else
269
+ []
270
+ end
271
+
272
+ context = build_hook_context(
273
+ event: :user_prompt,
274
+ metadata: {
275
+ prompt: prompt,
276
+ message_count: messages.size,
277
+ model: model.id,
278
+ provider: model.provider,
279
+ tools: actual_tools,
280
+ delegates_to: delegate_agents,
281
+ timestamp: Time.now.utc.iso8601,
282
+ },
283
+ )
284
+
285
+ agent_hooks = @hook_agent_hooks[:user_prompt] || []
286
+
287
+ result = @hook_executor.execute_safe(
288
+ event: :user_prompt,
289
+ context: context,
290
+ callbacks: agent_hooks,
291
+ )
292
+
293
+ # Handle hook result
294
+ if result.halt?
295
+ # Hook blocked execution
296
+ { halted: true, halt_message: result.value }
297
+ elsif result.replace?
298
+ # Hook provided stdout to append to prompt (exit code 0)
299
+ modified_prompt = "#{prompt}\n\n<hook-context>\n#{result.value}\n</hook-context>"
300
+ { halted: false, modified_prompt: modified_prompt }
301
+ else
302
+ # Normal continue
303
+ { halted: false, modified_prompt: prompt }
304
+ end
305
+ end
306
+
307
+ # Build a hook context object
308
+ #
309
+ # @param event [Symbol] Event type
310
+ # @param tool_call [Hooks::ToolCall, nil] Tool call object
311
+ # @param tool_result [Hooks::ToolResult, nil] Tool result object
312
+ # @param metadata [Hash] Additional metadata
313
+ # @return [Hooks::Context] Context object
314
+ def build_hook_context(event:, tool_call: nil, tool_result: nil, metadata: {})
315
+ Hooks::Context.new(
316
+ event: event,
317
+ agent_name: @agent_context&.name || "unknown",
318
+ agent_definition: nil, # Could store this in setup_hooks if needed
319
+ swarm: @hook_swarm,
320
+ tool_call: tool_call,
321
+ tool_result: tool_result,
322
+ metadata: metadata,
323
+ )
324
+ end
325
+
326
+ # Wrap a RubyLLM tool call in our Hooks::ToolCall value object
327
+ #
328
+ # @param tool_call [RubyLLM::ToolCall] RubyLLM tool call
329
+ # @return [Hooks::ToolCall] Our wrapped tool call
330
+ def wrap_tool_call_to_hooks(tool_call)
331
+ Hooks::ToolCall.new(
332
+ id: tool_call.id,
333
+ name: tool_call.name,
334
+ parameters: tool_call.arguments,
335
+ )
336
+ end
337
+
338
+ # Wrap a tool result in our Hooks::ToolResult value object
339
+ #
340
+ # @param tool_call_id [String] Tool call ID
341
+ # @param tool_name [String] Tool name
342
+ # @param result [Object] Tool execution result
343
+ # @return [Hooks::ToolResult] Our wrapped result
344
+ def wrap_tool_result(tool_call_id, tool_name, result)
345
+ success = !result.is_a?(StandardError)
346
+ error = result.is_a?(StandardError) ? result.message : nil
347
+
348
+ Hooks::ToolResult.new(
349
+ tool_call_id: tool_call_id,
350
+ tool_name: tool_name,
351
+ content: success ? result : nil,
352
+ success: success,
353
+ error: error,
354
+ )
355
+ end
356
+
357
+ # Check if a tool call is a delegation tool
358
+ #
359
+ # Delegation tools fire their own pre_delegation/post_delegation events
360
+ # and should NOT fire pre_tool_use/post_tool_use events.
361
+ #
362
+ # @param tool_call [RubyLLM::ToolCall] Tool call to check
363
+ # @return [Boolean] true if this is a delegation tool
364
+ def delegation_tool_call?(tool_call)
365
+ return false unless @agent_context
366
+
367
+ @agent_context.delegation_tool?(tool_call.name)
368
+ end
369
+ end
370
+ end
371
+ end
372
+ end
@@ -0,0 +1,99 @@
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
+ model_info = RubyLLM.models.find(message.model_id)
74
+ return zero_cost unless model_info
75
+
76
+ # Prices are per million tokens (USD)
77
+ input_cost = (message.input_tokens / 1_000_000.0) * model_info.input_price_per_million
78
+ output_cost = (message.output_tokens / 1_000_000.0) * model_info.output_price_per_million
79
+
80
+ {
81
+ input_cost: input_cost,
82
+ output_cost: output_cost,
83
+ total_cost: input_cost + output_cost,
84
+ }
85
+ rescue StandardError
86
+ # Model not found in registry or pricing not available
87
+ zero_cost
88
+ end
89
+
90
+ # Zero cost fallback
91
+ #
92
+ # @return [Hash] Zero cost breakdown
93
+ def zero_cost
94
+ { input_cost: 0.0, output_cost: 0.0, total_cost: 0.0 }
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,114 @@
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
+ # @param chat [Agent::Chat] The chat instance
60
+ # @param prompt [String] The user's actual prompt
61
+ # @return [void]
62
+ def inject_first_message_reminders(chat, prompt)
63
+ chat.add_message(role: :user, content: BEFORE_FIRST_MESSAGE_REMINDER)
64
+ chat.add_message(role: :user, content: prompt)
65
+ chat.add_message(role: :user, content: AFTER_FIRST_MESSAGE_REMINDER)
66
+ end
67
+
68
+ # Check if we should inject a periodic TodoWrite reminder
69
+ #
70
+ # Injects a reminder if:
71
+ # 1. Enough messages have passed (>= 5)
72
+ # 2. TodoWrite hasn't been used in the last TODOWRITE_REMINDER_INTERVAL messages
73
+ #
74
+ # @param chat [Agent::Chat] The chat instance
75
+ # @param last_todowrite_index [Integer, nil] Index of last TodoWrite usage
76
+ # @return [Boolean] true if reminder should be injected
77
+ def should_inject_todowrite_reminder?(chat, last_todowrite_index)
78
+ # Need at least a few messages before reminding
79
+ return false if chat.messages.count < 5
80
+
81
+ # Find the last message that contains TodoWrite tool usage
82
+ last_todo_index = chat.messages.rindex do |msg|
83
+ msg.role == :tool && msg.content.to_s.include?("TodoWrite")
84
+ end
85
+
86
+ # Check if enough messages have passed since last TodoWrite
87
+ if last_todo_index.nil? && last_todowrite_index.nil?
88
+ # Never used TodoWrite - check if we've exceeded interval
89
+ chat.messages.count >= TODOWRITE_REMINDER_INTERVAL
90
+ elsif last_todo_index
91
+ # Recently used - don't remind
92
+ false
93
+ elsif last_todowrite_index
94
+ # Used before - check if interval has passed
95
+ chat.messages.count - last_todowrite_index >= TODOWRITE_REMINDER_INTERVAL
96
+ else
97
+ false
98
+ end
99
+ end
100
+
101
+ # Update the last TodoWrite index by finding it in messages
102
+ #
103
+ # @param chat [Agent::Chat] The chat instance
104
+ # @return [Integer, nil] Index of last TodoWrite usage, or nil
105
+ def find_last_todowrite_index(chat)
106
+ chat.messages.rindex do |msg|
107
+ msg.role == :tool && msg.content.to_s.include?("TodoWrite")
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
114
+ end