claude_swarm 1.0.9 → 1.0.11

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 (134) hide show
  1. checksums.yaml +4 -4
  2. data/{CHANGELOG.md → CHANGELOG.claude-swarm.md} +10 -0
  3. data/CLAUDE.md +346 -191
  4. data/decisions/2025-11-22-001-global-agent-registry.md +172 -0
  5. data/docs/v2/CHANGELOG.swarm_cli.md +20 -0
  6. data/docs/v2/CHANGELOG.swarm_memory.md +146 -1
  7. data/docs/v2/CHANGELOG.swarm_sdk.md +433 -10
  8. data/docs/v2/README.md +20 -5
  9. data/docs/v2/guides/complete-tutorial.md +95 -9
  10. data/docs/v2/guides/getting-started.md +10 -8
  11. data/docs/v2/guides/memory-adapters.md +41 -0
  12. data/docs/v2/guides/migrating-to-2.x.md +746 -0
  13. data/docs/v2/guides/plugins.md +52 -5
  14. data/docs/v2/guides/rails-integration.md +6 -0
  15. data/docs/v2/guides/snapshots.md +14 -14
  16. data/docs/v2/guides/swarm-memory.md +2 -13
  17. data/docs/v2/reference/architecture-flow.md +3 -3
  18. data/docs/v2/reference/cli.md +0 -1
  19. data/docs/v2/reference/configuration_reference.md +300 -0
  20. data/docs/v2/reference/event_payload_structures.md +27 -5
  21. data/docs/v2/reference/ruby-dsl.md +614 -18
  22. data/docs/v2/reference/swarm_memory_technical_details.md +7 -29
  23. data/docs/v2/reference/yaml.md +172 -54
  24. data/examples/snapshot_demo.rb +2 -2
  25. data/lib/claude_swarm/mcp_generator.rb +8 -21
  26. data/lib/claude_swarm/orchestrator.rb +8 -1
  27. data/lib/claude_swarm/version.rb +1 -1
  28. data/lib/swarm_cli/commands/run.rb +2 -2
  29. data/lib/swarm_cli/config_loader.rb +11 -11
  30. data/lib/swarm_cli/formatters/human_formatter.rb +0 -33
  31. data/lib/swarm_cli/interactive_repl.rb +2 -2
  32. data/lib/swarm_cli/ui/icons.rb +0 -23
  33. data/lib/swarm_cli/version.rb +1 -1
  34. data/lib/swarm_memory/adapters/filesystem_adapter.rb +11 -34
  35. data/lib/swarm_memory/core/semantic_index.rb +10 -2
  36. data/lib/swarm_memory/core/storage.rb +7 -2
  37. data/lib/swarm_memory/dsl/memory_config.rb +37 -0
  38. data/lib/swarm_memory/integration/sdk_plugin.rb +201 -28
  39. data/lib/swarm_memory/optimization/defragmenter.rb +1 -1
  40. data/lib/swarm_memory/prompts/memory_researcher.md.erb +0 -1
  41. data/lib/swarm_memory/tools/load_skill.rb +0 -1
  42. data/lib/swarm_memory/tools/memory_edit.rb +2 -1
  43. data/lib/swarm_memory/tools/memory_read.rb +1 -1
  44. data/lib/swarm_memory/version.rb +1 -1
  45. data/lib/swarm_memory.rb +8 -6
  46. data/lib/swarm_sdk/agent/builder.rb +58 -0
  47. data/lib/swarm_sdk/agent/chat.rb +527 -1061
  48. data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +13 -88
  49. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
  50. data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +108 -46
  51. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
  52. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +267 -0
  53. data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +3 -3
  54. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
  55. data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +11 -13
  56. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
  57. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +146 -0
  58. data/lib/swarm_sdk/agent/context.rb +1 -2
  59. data/lib/swarm_sdk/agent/definition.rb +66 -154
  60. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +4 -2
  61. data/lib/swarm_sdk/agent/system_prompt_builder.rb +161 -0
  62. data/lib/swarm_sdk/agent_registry.rb +146 -0
  63. data/lib/swarm_sdk/builders/base_builder.rb +488 -0
  64. data/lib/swarm_sdk/concerns/cleanupable.rb +39 -0
  65. data/lib/swarm_sdk/concerns/snapshotable.rb +67 -0
  66. data/lib/swarm_sdk/concerns/validatable.rb +55 -0
  67. data/lib/swarm_sdk/config.rb +302 -0
  68. data/lib/swarm_sdk/configuration/parser.rb +373 -0
  69. data/lib/swarm_sdk/configuration/translator.rb +255 -0
  70. data/lib/swarm_sdk/configuration.rb +77 -546
  71. data/lib/swarm_sdk/context_compactor/token_counter.rb +2 -6
  72. data/lib/swarm_sdk/context_compactor.rb +6 -11
  73. data/lib/swarm_sdk/context_management/builder.rb +128 -0
  74. data/lib/swarm_sdk/context_management/context.rb +328 -0
  75. data/lib/swarm_sdk/custom_tool_registry.rb +226 -0
  76. data/lib/swarm_sdk/defaults.rb +196 -0
  77. data/lib/swarm_sdk/events_to_messages.rb +18 -0
  78. data/lib/swarm_sdk/hooks/adapter.rb +3 -3
  79. data/lib/swarm_sdk/hooks/shell_executor.rb +4 -2
  80. data/lib/swarm_sdk/log_collector.rb +179 -29
  81. data/lib/swarm_sdk/log_stream.rb +29 -0
  82. data/lib/swarm_sdk/models.json +4333 -1
  83. data/lib/swarm_sdk/models.rb +43 -2
  84. data/lib/swarm_sdk/node_context.rb +1 -1
  85. data/lib/swarm_sdk/observer/builder.rb +81 -0
  86. data/lib/swarm_sdk/observer/config.rb +45 -0
  87. data/lib/swarm_sdk/observer/manager.rb +236 -0
  88. data/lib/swarm_sdk/patterns/agent_observer.rb +160 -0
  89. data/lib/swarm_sdk/plugin.rb +95 -5
  90. data/lib/swarm_sdk/result.rb +52 -0
  91. data/lib/swarm_sdk/snapshot.rb +6 -6
  92. data/lib/swarm_sdk/snapshot_from_events.rb +13 -2
  93. data/lib/swarm_sdk/state_restorer.rb +136 -151
  94. data/lib/swarm_sdk/state_snapshot.rb +65 -100
  95. data/lib/swarm_sdk/swarm/agent_initializer.rb +181 -137
  96. data/lib/swarm_sdk/swarm/builder.rb +44 -578
  97. data/lib/swarm_sdk/swarm/executor.rb +213 -0
  98. data/lib/swarm_sdk/swarm/hook_triggers.rb +151 -0
  99. data/lib/swarm_sdk/swarm/logging_callbacks.rb +341 -0
  100. data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
  101. data/lib/swarm_sdk/swarm/tool_configurator.rb +58 -140
  102. data/lib/swarm_sdk/swarm.rb +203 -683
  103. data/lib/swarm_sdk/tools/bash.rb +14 -8
  104. data/lib/swarm_sdk/tools/delegate.rb +61 -43
  105. data/lib/swarm_sdk/tools/edit.rb +8 -13
  106. data/lib/swarm_sdk/tools/glob.rb +12 -4
  107. data/lib/swarm_sdk/tools/grep.rb +7 -0
  108. data/lib/swarm_sdk/tools/multi_edit.rb +15 -11
  109. data/lib/swarm_sdk/tools/path_resolver.rb +51 -2
  110. data/lib/swarm_sdk/tools/read.rb +16 -18
  111. data/lib/swarm_sdk/tools/registry.rb +122 -10
  112. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +9 -5
  113. data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
  114. data/lib/swarm_sdk/tools/todo_write.rb +7 -0
  115. data/lib/swarm_sdk/tools/web_fetch.rb +20 -17
  116. data/lib/swarm_sdk/tools/write.rb +8 -13
  117. data/lib/swarm_sdk/version.rb +1 -1
  118. data/lib/swarm_sdk/{node → workflow}/agent_config.rb +1 -1
  119. data/lib/swarm_sdk/workflow/builder.rb +192 -0
  120. data/lib/swarm_sdk/workflow/executor.rb +497 -0
  121. data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +7 -5
  122. data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +5 -3
  123. data/lib/swarm_sdk/{node_orchestrator.rb → workflow.rb} +152 -456
  124. data/lib/swarm_sdk.rb +294 -108
  125. data/rubocop/cop/security/no_reflection_methods.rb +1 -1
  126. data/swarm_cli.gemspec +1 -1
  127. data/swarm_memory.gemspec +8 -3
  128. data/swarm_sdk.gemspec +6 -4
  129. data/team_full.yml +124 -320
  130. metadata +42 -14
  131. data/lib/swarm_memory/chat_extension.rb +0 -34
  132. data/lib/swarm_memory/tools/memory_multi_edit.rb +0 -281
  133. data/lib/swarm_sdk/providers/openai_with_responses.rb +0 -589
  134. /data/lib/swarm_memory/{errors.rb → error.rb} +0 -0
@@ -17,10 +17,6 @@ module SwarmSDK
17
17
  # total_tokens = TokenCounter.estimate_messages(messages)
18
18
  #
19
19
  class TokenCounter
20
- # Average characters per token for different content types
21
- CHARS_PER_TOKEN_PROSE = 4.0
22
- CHARS_PER_TOKEN_CODE = 3.5
23
-
24
20
  class << self
25
21
  # Estimate tokens for a single message
26
22
  #
@@ -78,9 +74,9 @@ module SwarmSDK
78
74
 
79
75
  # Choose characters per token based on content type
80
76
  chars_per_token = if code_ratio > 0.1
81
- CHARS_PER_TOKEN_CODE # Code
77
+ SwarmSDK.config.chars_per_token_code # Code
82
78
  else
83
- CHARS_PER_TOKEN_PROSE # Prose
79
+ SwarmSDK.config.chars_per_token_prose # Prose
84
80
  end
85
81
 
86
82
  (text.length / chars_per_token).ceil
@@ -58,7 +58,7 @@ module SwarmSDK
58
58
  # @return [ContextCompactor::Metrics] Compression metrics
59
59
  def compact
60
60
  start_time = Time.now
61
- original_messages = @chat.messages.dup
61
+ original_messages = @chat.messages
62
62
 
63
63
  # Emit compression_started event
64
64
  LogStream.emit(
@@ -308,7 +308,8 @@ module SwarmSDK
308
308
  response.content
309
309
  rescue StandardError => e
310
310
  # If summarization fails, create a simple fallback summary
311
- RubyLLM.logger.warn("ContextCompactor: Summarization failed: #{e.message}")
311
+ LogStream.emit_error(e, source: "context_compactor", context: "generate_summary", agent: @agent_name)
312
+ RubyLLM.logger.debug("ContextCompactor: Summarization failed: #{e.message}")
312
313
 
313
314
  <<~FALLBACK
314
315
  ## Summary
@@ -322,19 +323,13 @@ module SwarmSDK
322
323
 
323
324
  # Replace messages in the chat
324
325
  #
325
- # RubyLLM::Chat doesn't have a public API for replacing all messages,
326
- # so we need to work with the internal messages array.
326
+ # Delegates to the Chat's replace_messages method which provides
327
+ # a safe abstraction over the internal message array.
327
328
  #
328
329
  # @param new_messages [Array<RubyLLM::Message>] New message array
329
330
  # @return [void]
330
331
  def replace_messages(new_messages)
331
- # Clear existing messages
332
- @chat.messages.clear
333
-
334
- # Add new messages
335
- new_messages.each do |msg|
336
- @chat.messages << msg
337
- end
332
+ @chat.replace_messages(new_messages)
338
333
  end
339
334
  end
340
335
  end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module ContextManagement
5
+ # DSL for defining context management handlers
6
+ #
7
+ # This builder provides a clean, idiomatic way to register handlers for
8
+ # context warning thresholds. Handlers receive a rich context object
9
+ # with message manipulation methods.
10
+ #
11
+ # @example Basic usage
12
+ # context_management do
13
+ # on :warning_60 do |ctx|
14
+ # ctx.compress_tool_results(keep_recent: 10)
15
+ # end
16
+ #
17
+ # on :warning_80 do |ctx|
18
+ # ctx.prune_old_messages(keep_recent: 20)
19
+ # end
20
+ # end
21
+ #
22
+ # @example Progressive compression
23
+ # context_management do
24
+ # on :warning_60 do |ctx|
25
+ # ctx.compress_tool_results(keep_recent: 15, truncate_to: 500)
26
+ # end
27
+ #
28
+ # on :warning_80 do |ctx|
29
+ # ctx.prune_old_messages(keep_recent: 30)
30
+ # ctx.compress_tool_results(keep_recent: 5, truncate_to: 200)
31
+ # end
32
+ #
33
+ # on :warning_90 do |ctx|
34
+ # ctx.log_action("emergency_pruning", tokens_remaining: ctx.tokens_remaining)
35
+ # ctx.prune_old_messages(keep_recent: 15)
36
+ # end
37
+ # end
38
+ class Builder
39
+ # Map semantic event names to threshold percentages
40
+ EVENT_MAP = {
41
+ warning_60: 60,
42
+ warning_80: 80,
43
+ warning_90: 90,
44
+ }.freeze
45
+
46
+ def initialize
47
+ @handlers = {} # { threshold => block }
48
+ end
49
+
50
+ # Register a handler for a context warning threshold
51
+ #
52
+ # Handlers take full responsibility for managing context at their threshold.
53
+ # When a handler is registered for a threshold, automatic compression is disabled
54
+ # for that threshold.
55
+ #
56
+ # @param event [Symbol] Event name (:warning_60, :warning_80, :warning_90)
57
+ # @yield [ContextManagement::Context] Context with message manipulation methods
58
+ # @return [void]
59
+ #
60
+ # @raise [ArgumentError] If event is unknown or block is missing
61
+ #
62
+ # @example Compress tool results at 60%
63
+ # on :warning_60 do |ctx|
64
+ # ctx.compress_tool_results(keep_recent: 10)
65
+ # end
66
+ #
67
+ # @example Custom logic at 80%
68
+ # on :warning_80 do |ctx|
69
+ # if ctx.usage_percentage > 85
70
+ # ctx.prune_old_messages(keep_recent: 10)
71
+ # else
72
+ # ctx.summarize_old_exchanges(older_than: 20)
73
+ # end
74
+ # end
75
+ #
76
+ # @example Log and prune at 90%
77
+ # on :warning_90 do |ctx|
78
+ # ctx.log_action("critical_threshold", remaining: ctx.tokens_remaining)
79
+ # ctx.prune_old_messages(keep_recent: 10)
80
+ # end
81
+ def on(event, &block)
82
+ threshold = EVENT_MAP[event]
83
+ raise ArgumentError, "Unknown event: #{event}. Valid events: #{EVENT_MAP.keys.join(", ")}" unless threshold
84
+ raise ArgumentError, "Block required for #{event}" unless block
85
+
86
+ @handlers[threshold] = block
87
+ end
88
+
89
+ # Build hook definitions from handlers
90
+ #
91
+ # Creates Hooks::Definition objects that wrap user blocks to provide
92
+ # rich context objects instead of raw Hooks::Context. Each handler
93
+ # becomes a hook for the :context_warning event.
94
+ #
95
+ # @return [Array<Hooks::Definition>] Hook definitions for :context_warning event
96
+ def build
97
+ @handlers.map do |threshold, user_block|
98
+ # Create a hook that filters by threshold and wraps context
99
+ Hooks::Definition.new(
100
+ event: :context_warning,
101
+ matcher: nil, # No tool matching needed
102
+ priority: 0,
103
+ proc: create_threshold_matcher(threshold, user_block),
104
+ )
105
+ end
106
+ end
107
+
108
+ private
109
+
110
+ # Create a proc that matches threshold and wraps context
111
+ #
112
+ # @param target_threshold [Integer] Threshold to match (60, 80, 90)
113
+ # @param user_block [Proc] User's handler block
114
+ # @return [Proc] Hook proc
115
+ def create_threshold_matcher(target_threshold, user_block)
116
+ proc do |hooks_context|
117
+ # Only execute for matching threshold
118
+ current_threshold = hooks_context.metadata[:threshold]
119
+ next unless current_threshold == target_threshold
120
+
121
+ # Wrap in rich context object
122
+ rich_context = Context.new(hooks_context)
123
+ user_block.call(rich_context)
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,328 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module ContextManagement
5
+ # Rich context wrapper for context management handlers
6
+ #
7
+ # Provides a clean, developer-friendly API for manipulating the conversation
8
+ # context when warning thresholds are triggered. Wraps the lower-level
9
+ # Hooks::Context with message manipulation helpers.
10
+ #
11
+ # @example Basic usage in handler
12
+ # on :warning_60 do |ctx|
13
+ # ctx.compress_tool_results(keep_recent: 10)
14
+ # end
15
+ #
16
+ # @example Advanced usage with metrics
17
+ # on :warning_80 do |ctx|
18
+ # if ctx.usage_percentage > 85
19
+ # ctx.prune_old_messages(keep_recent: 10)
20
+ # ctx.log_action("aggressive_pruning", remaining: ctx.tokens_remaining)
21
+ # else
22
+ # ctx.compress_tool_results(keep_recent: 5, truncate_to: 100)
23
+ # end
24
+ # end
25
+ class Context
26
+ # Create a new context wrapper
27
+ #
28
+ # @param hooks_context [Hooks::Context] Lower-level hook context with metadata
29
+ def initialize(hooks_context)
30
+ @hooks_context = hooks_context
31
+ @chat = hooks_context.metadata[:chat]
32
+ end
33
+
34
+ # --- Context Metrics ---
35
+
36
+ # Current context usage percentage
37
+ #
38
+ # @return [Float] Usage percentage (0.0 to 100.0)
39
+ #
40
+ # @example
41
+ # if ctx.usage_percentage > 85
42
+ # ctx.prune_old_messages(keep_recent: 10)
43
+ # end
44
+ def usage_percentage
45
+ @hooks_context.metadata[:percentage]
46
+ end
47
+
48
+ # Threshold that triggered this handler
49
+ #
50
+ # @return [Integer] Threshold (60, 80, or 90)
51
+ #
52
+ # @example
53
+ # ctx.log_action("threshold_hit", threshold: ctx.threshold)
54
+ def threshold
55
+ @hooks_context.metadata[:threshold]
56
+ end
57
+
58
+ # Total tokens used so far
59
+ #
60
+ # @return [Integer] Token count
61
+ #
62
+ # @example
63
+ # ctx.log_action("usage", tokens: ctx.tokens_used)
64
+ def tokens_used
65
+ @hooks_context.metadata[:tokens_used]
66
+ end
67
+
68
+ # Tokens remaining in context window
69
+ #
70
+ # @return [Integer] Token count
71
+ #
72
+ # @example
73
+ # if ctx.tokens_remaining < 10000
74
+ # ctx.prune_old_messages(keep_recent: 5)
75
+ # end
76
+ def tokens_remaining
77
+ @hooks_context.metadata[:tokens_remaining]
78
+ end
79
+
80
+ # Total context window size
81
+ #
82
+ # @return [Integer] Token count
83
+ #
84
+ # @example
85
+ # buffer = ctx.context_limit * 0.1 # 10% buffer
86
+ def context_limit
87
+ @hooks_context.metadata[:context_limit]
88
+ end
89
+
90
+ # Agent name
91
+ #
92
+ # @return [Symbol] Agent identifier
93
+ #
94
+ # @example
95
+ # ctx.log_action("agent_context", agent: ctx.agent_name)
96
+ def agent_name
97
+ @hooks_context.agent_name
98
+ end
99
+
100
+ # --- Message Access ---
101
+
102
+ # Get all messages (copy for manipulation)
103
+ #
104
+ # @return [Array<RubyLLM::Message>] Message array
105
+ #
106
+ # @example
107
+ # ctx.messages.each do |msg|
108
+ # puts "#{msg.role}: #{msg.content.length} chars"
109
+ # end
110
+ def messages
111
+ @chat.messages
112
+ end
113
+
114
+ # Number of messages
115
+ #
116
+ # @return [Integer] Message count
117
+ #
118
+ # @example
119
+ # if ctx.message_count > 100
120
+ # ctx.prune_old_messages(keep_recent: 50)
121
+ # end
122
+ def message_count
123
+ @chat.message_count
124
+ end
125
+
126
+ # --- Message Manipulation ---
127
+
128
+ # Replace all messages with new array
129
+ #
130
+ # @param new_messages [Array<RubyLLM::Message>] New message array
131
+ # @return [void]
132
+ #
133
+ # @example
134
+ # new_msgs = ctx.messages.reject { |m| m.role == :tool }
135
+ # ctx.replace_messages(new_msgs)
136
+ def replace_messages(new_messages)
137
+ @chat.replace_messages(new_messages)
138
+ end
139
+
140
+ # Compress tool result messages to save context space
141
+ #
142
+ # Creates NEW message objects with truncated content (follows RubyLLM patterns).
143
+ # Truncates old tool results while keeping recent ones intact.
144
+ # Automatically marks compression as applied to prevent double compression.
145
+ #
146
+ # @param keep_recent [Integer] Number of recent tool results to preserve (default: 10)
147
+ # @param truncate_to [Integer] Max characters for truncated results (default: 200)
148
+ # @return [Integer] Number of messages compressed
149
+ #
150
+ # @example Light compression at 60%
151
+ # ctx.compress_tool_results(keep_recent: 15, truncate_to: 500)
152
+ #
153
+ # @example Aggressive compression at 80%
154
+ # ctx.compress_tool_results(keep_recent: 5, truncate_to: 100)
155
+ def compress_tool_results(keep_recent: 10, truncate_to: 200)
156
+ msgs = messages.dup
157
+ compressed_count = 0
158
+
159
+ # Find tool result messages (skip recent ones)
160
+ tool_indices = []
161
+ msgs.each_with_index do |msg, idx|
162
+ tool_indices << idx if msg.role == :tool
163
+ end
164
+
165
+ # Keep recent tool results, compress older ones
166
+ indices_to_compress = tool_indices[0...-keep_recent] || []
167
+
168
+ indices_to_compress.each do |idx|
169
+ msg = msgs[idx]
170
+ content = msg.content.to_s
171
+ next if content.length <= truncate_to
172
+
173
+ # Create NEW message with truncated content (NO instance_variable_set!)
174
+ truncated_content = "#{content[0...truncate_to]}... [truncated for context management]"
175
+
176
+ # Create new message object following RubyLLM patterns
177
+ msgs[idx] = RubyLLM::Message.new(
178
+ role: :tool,
179
+ content: truncated_content,
180
+ tool_call_id: msg.tool_call_id,
181
+ )
182
+ compressed_count += 1
183
+ end
184
+
185
+ replace_messages(msgs)
186
+
187
+ # Mark compression as applied to coordinate with ContextManager
188
+ mark_compression_applied
189
+
190
+ compressed_count
191
+ end
192
+
193
+ # Mark compression as applied in ContextManager
194
+ #
195
+ # Call this when your handler performs compression to prevent
196
+ # double compression from auto-compression logic.
197
+ #
198
+ # @return [void]
199
+ #
200
+ # @example Custom compression
201
+ # msgs = ctx.messages.map { |m| ... } # custom logic
202
+ # ctx.replace_messages(msgs)
203
+ # ctx.mark_compression_applied
204
+ def mark_compression_applied
205
+ return unless @chat.respond_to?(:context_manager)
206
+
207
+ @chat.context_manager.compression_applied = true
208
+ end
209
+
210
+ # Check if compression has already been applied
211
+ #
212
+ # @return [Boolean] True if compression was already applied
213
+ #
214
+ # @example Conditional compression
215
+ # unless ctx.compression_applied?
216
+ # ctx.compress_tool_results(keep_recent: 10)
217
+ # end
218
+ def compression_applied?
219
+ return false unless @chat.respond_to?(:context_manager)
220
+
221
+ !!@chat.context_manager.compression_applied
222
+ end
223
+
224
+ # Remove old messages from history
225
+ #
226
+ # Keeps system message (if any) and recent exchanges.
227
+ # This is more aggressive than compression and loses context.
228
+ #
229
+ # @param keep_recent [Integer] Number of recent messages to keep (default: 20)
230
+ # @return [Integer] Number of messages removed
231
+ #
232
+ # @example Prune at 80% threshold
233
+ # ctx.prune_old_messages(keep_recent: 30)
234
+ #
235
+ # @example Emergency pruning at 90%
236
+ # ctx.prune_old_messages(keep_recent: 10)
237
+ def prune_old_messages(keep_recent: 20)
238
+ msgs = messages.dup
239
+ original_count = msgs.size
240
+
241
+ # Always keep system message if present
242
+ system_msg = msgs.first if msgs.first&.role == :system
243
+ non_system = system_msg ? msgs[1..] : msgs
244
+
245
+ # Keep only recent messages
246
+ if non_system.size > keep_recent
247
+ kept = non_system.last(keep_recent)
248
+ new_msgs = system_msg ? [system_msg] + kept : kept
249
+ replace_messages(new_msgs)
250
+ original_count - new_msgs.size
251
+ else
252
+ 0
253
+ end
254
+ end
255
+
256
+ # Summarize old message exchanges
257
+ #
258
+ # Groups old user/assistant pairs and replaces with summary.
259
+ # This is a placeholder - actual implementation would use LLM.
260
+ #
261
+ # @param older_than [Integer] Messages older than this index get summarized
262
+ # @return [Integer] Number of exchanges summarized
263
+ #
264
+ # @example
265
+ # ctx.summarize_old_exchanges(older_than: 10)
266
+ def summarize_old_exchanges(older_than: 10)
267
+ # For now, this is a marker - full implementation would call LLM
268
+ # to summarize exchanges. We provide the API for developers to
269
+ # implement their own summarization logic.
270
+ 0
271
+ end
272
+
273
+ # Custom message transformation
274
+ #
275
+ # Apply a block to transform messages. This gives full control
276
+ # over message manipulation for custom strategies.
277
+ #
278
+ # @yield [Array<RubyLLM::Message>] Current messages
279
+ # @yieldreturn [Array<RubyLLM::Message>] Transformed messages
280
+ # @return [void]
281
+ #
282
+ # @example Remove specific tool results
283
+ # ctx.transform_messages do |msgs|
284
+ # msgs.reject { |m| m.role == :tool && m.content.include?("verbose output") }
285
+ # end
286
+ #
287
+ # @example Custom compression logic
288
+ # ctx.transform_messages do |msgs|
289
+ # msgs.map do |m|
290
+ # if m.role == :tool && m.content.length > 1000
291
+ # RubyLLM::Message.new(role: :tool, content: m.content[0..500], tool_call_id: m.tool_call_id)
292
+ # else
293
+ # m
294
+ # end
295
+ # end
296
+ # end
297
+ def transform_messages
298
+ new_msgs = yield(messages.dup)
299
+ replace_messages(new_msgs)
300
+ end
301
+
302
+ # Log a context management action
303
+ #
304
+ # Emits a log event for tracking what actions were taken.
305
+ # Useful for debugging and monitoring context management strategies.
306
+ #
307
+ # @param action [String] Description of action taken
308
+ # @param details [Hash] Additional details
309
+ # @return [void]
310
+ #
311
+ # @example Log compression action
312
+ # ctx.log_action("compressed_tool_results", count: 5)
313
+ #
314
+ # @example Log emergency action
315
+ # ctx.log_action("emergency_pruning", remaining: ctx.tokens_remaining)
316
+ def log_action(action, details = {})
317
+ LogStream.emit(
318
+ type: "context_management_action",
319
+ agent: agent_name,
320
+ threshold: threshold,
321
+ action: action,
322
+ usage_percentage: usage_percentage,
323
+ **details,
324
+ )
325
+ end
326
+ end
327
+ end
328
+ end