swarm_sdk 2.7.13 → 3.0.0.alpha1

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 (183) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_sdk/ruby_llm_patches/chat_callbacks_patch.rb +43 -22
  3. data/lib/swarm_sdk/ruby_llm_patches/init.rb +6 -0
  4. data/lib/swarm_sdk/ruby_llm_patches/mcp_ssl_patch.rb +144 -0
  5. data/lib/swarm_sdk/ruby_llm_patches/tool_concurrency_patch.rb +3 -4
  6. data/lib/swarm_sdk/v3/agent.rb +1165 -0
  7. data/lib/swarm_sdk/v3/agent_builder.rb +533 -0
  8. data/lib/swarm_sdk/v3/agent_definition.rb +330 -0
  9. data/lib/swarm_sdk/v3/configuration.rb +490 -0
  10. data/lib/swarm_sdk/v3/debug_log.rb +86 -0
  11. data/lib/swarm_sdk/v3/event_stream.rb +130 -0
  12. data/lib/swarm_sdk/v3/hooks/context.rb +112 -0
  13. data/lib/swarm_sdk/v3/hooks/result.rb +115 -0
  14. data/lib/swarm_sdk/v3/hooks/runner.rb +128 -0
  15. data/lib/swarm_sdk/v3/mcp/connector.rb +183 -0
  16. data/lib/swarm_sdk/v3/mcp/mcp_error.rb +15 -0
  17. data/lib/swarm_sdk/v3/mcp/server_definition.rb +125 -0
  18. data/lib/swarm_sdk/v3/mcp/ssl_http_transport.rb +103 -0
  19. data/lib/swarm_sdk/v3/mcp/stdio_transport.rb +135 -0
  20. data/lib/swarm_sdk/v3/mcp/tool_proxy.rb +53 -0
  21. data/lib/swarm_sdk/v3/memory/adapters/base.rb +297 -0
  22. data/lib/swarm_sdk/v3/memory/adapters/faiss_support.rb +194 -0
  23. data/lib/swarm_sdk/v3/memory/adapters/filesystem_adapter.rb +212 -0
  24. data/lib/swarm_sdk/v3/memory/adapters/sqlite_adapter.rb +507 -0
  25. data/lib/swarm_sdk/v3/memory/adapters/vector_utils.rb +88 -0
  26. data/lib/swarm_sdk/v3/memory/card.rb +206 -0
  27. data/lib/swarm_sdk/v3/memory/cluster.rb +146 -0
  28. data/lib/swarm_sdk/v3/memory/compressor.rb +496 -0
  29. data/lib/swarm_sdk/v3/memory/consolidator.rb +427 -0
  30. data/lib/swarm_sdk/v3/memory/context_builder.rb +339 -0
  31. data/lib/swarm_sdk/v3/memory/edge.rb +105 -0
  32. data/lib/swarm_sdk/v3/memory/embedder.rb +185 -0
  33. data/lib/swarm_sdk/v3/memory/exposure_tracker.rb +104 -0
  34. data/lib/swarm_sdk/v3/memory/ingestion_pipeline.rb +394 -0
  35. data/lib/swarm_sdk/v3/memory/retriever.rb +289 -0
  36. data/lib/swarm_sdk/v3/memory/store.rb +489 -0
  37. data/lib/swarm_sdk/v3/skills/loader.rb +147 -0
  38. data/lib/swarm_sdk/v3/skills/manifest.rb +45 -0
  39. data/lib/swarm_sdk/v3/sub_task_agent.rb +248 -0
  40. data/lib/swarm_sdk/v3/tools/base.rb +80 -0
  41. data/lib/swarm_sdk/v3/tools/bash.rb +174 -0
  42. data/lib/swarm_sdk/v3/tools/clock.rb +32 -0
  43. data/lib/swarm_sdk/v3/tools/edit.rb +111 -0
  44. data/lib/swarm_sdk/v3/tools/glob.rb +96 -0
  45. data/lib/swarm_sdk/v3/tools/grep.rb +200 -0
  46. data/lib/swarm_sdk/v3/tools/message_teammate.rb +15 -0
  47. data/lib/swarm_sdk/v3/tools/message_user.rb +15 -0
  48. data/lib/swarm_sdk/v3/tools/read.rb +181 -0
  49. data/lib/swarm_sdk/v3/tools/read_tracker.rb +40 -0
  50. data/lib/swarm_sdk/v3/tools/registry.rb +208 -0
  51. data/lib/swarm_sdk/v3/tools/sub_task.rb +183 -0
  52. data/lib/swarm_sdk/v3/tools/think.rb +88 -0
  53. data/lib/swarm_sdk/v3/tools/write.rb +87 -0
  54. data/lib/swarm_sdk/v3.rb +145 -0
  55. metadata +84 -148
  56. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -175
  57. data/lib/swarm_sdk/agent/builder.rb +0 -680
  58. data/lib/swarm_sdk/agent/chat.rb +0 -1432
  59. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -375
  60. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  61. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  62. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -85
  63. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -290
  64. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  65. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  66. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -134
  67. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  68. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -146
  69. data/lib/swarm_sdk/agent/context.rb +0 -115
  70. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  71. data/lib/swarm_sdk/agent/definition.rb +0 -581
  72. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -226
  73. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -161
  74. data/lib/swarm_sdk/agent/tool_registry.rb +0 -189
  75. data/lib/swarm_sdk/agent_registry.rb +0 -146
  76. data/lib/swarm_sdk/builders/base_builder.rb +0 -553
  77. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  78. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -39
  79. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  80. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  81. data/lib/swarm_sdk/config.rb +0 -367
  82. data/lib/swarm_sdk/configuration/parser.rb +0 -397
  83. data/lib/swarm_sdk/configuration/translator.rb +0 -283
  84. data/lib/swarm_sdk/configuration.rb +0 -165
  85. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  86. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -102
  87. data/lib/swarm_sdk/context_compactor.rb +0 -335
  88. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  89. data/lib/swarm_sdk/context_management/context.rb +0 -328
  90. data/lib/swarm_sdk/custom_tool_registry.rb +0 -226
  91. data/lib/swarm_sdk/defaults.rb +0 -251
  92. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  93. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  94. data/lib/swarm_sdk/hooks/context.rb +0 -197
  95. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  96. data/lib/swarm_sdk/hooks/error.rb +0 -29
  97. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  98. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  99. data/lib/swarm_sdk/hooks/result.rb +0 -150
  100. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -256
  101. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  102. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  103. data/lib/swarm_sdk/log_collector.rb +0 -227
  104. data/lib/swarm_sdk/log_stream.rb +0 -127
  105. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  106. data/lib/swarm_sdk/model_aliases.json +0 -8
  107. data/lib/swarm_sdk/models.json +0 -44002
  108. data/lib/swarm_sdk/models.rb +0 -161
  109. data/lib/swarm_sdk/node_context.rb +0 -245
  110. data/lib/swarm_sdk/observer/builder.rb +0 -81
  111. data/lib/swarm_sdk/observer/config.rb +0 -45
  112. data/lib/swarm_sdk/observer/manager.rb +0 -236
  113. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  114. data/lib/swarm_sdk/permissions/config.rb +0 -239
  115. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  116. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  117. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  118. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  119. data/lib/swarm_sdk/plugin.rb +0 -309
  120. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  121. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  122. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -117
  123. data/lib/swarm_sdk/restore_result.rb +0 -65
  124. data/lib/swarm_sdk/result.rb +0 -212
  125. data/lib/swarm_sdk/snapshot.rb +0 -156
  126. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  127. data/lib/swarm_sdk/state_restorer.rb +0 -476
  128. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  129. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -648
  130. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -195
  131. data/lib/swarm_sdk/swarm/builder.rb +0 -256
  132. data/lib/swarm_sdk/swarm/executor.rb +0 -290
  133. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -151
  134. data/lib/swarm_sdk/swarm/lazy_delegate_chat.rb +0 -372
  135. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -360
  136. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -270
  137. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  138. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -392
  139. data/lib/swarm_sdk/swarm.rb +0 -843
  140. data/lib/swarm_sdk/swarm_loader.rb +0 -145
  141. data/lib/swarm_sdk/swarm_registry.rb +0 -136
  142. data/lib/swarm_sdk/tools/base.rb +0 -63
  143. data/lib/swarm_sdk/tools/bash.rb +0 -280
  144. data/lib/swarm_sdk/tools/clock.rb +0 -46
  145. data/lib/swarm_sdk/tools/delegate.rb +0 -389
  146. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
  147. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
  148. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
  149. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
  150. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
  151. data/lib/swarm_sdk/tools/edit.rb +0 -145
  152. data/lib/swarm_sdk/tools/glob.rb +0 -166
  153. data/lib/swarm_sdk/tools/grep.rb +0 -235
  154. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
  155. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -167
  156. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  157. data/lib/swarm_sdk/tools/mcp_tool_stub.rb +0 -198
  158. data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
  159. data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
  160. data/lib/swarm_sdk/tools/read.rb +0 -261
  161. data/lib/swarm_sdk/tools/registry.rb +0 -205
  162. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
  163. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
  164. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
  165. data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
  166. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -273
  167. data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
  168. data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
  169. data/lib/swarm_sdk/tools/think.rb +0 -100
  170. data/lib/swarm_sdk/tools/todo_write.rb +0 -237
  171. data/lib/swarm_sdk/tools/web_fetch.rb +0 -264
  172. data/lib/swarm_sdk/tools/write.rb +0 -112
  173. data/lib/swarm_sdk/transcript_builder.rb +0 -278
  174. data/lib/swarm_sdk/utils.rb +0 -68
  175. data/lib/swarm_sdk/validation_result.rb +0 -33
  176. data/lib/swarm_sdk/version.rb +0 -5
  177. data/lib/swarm_sdk/workflow/agent_config.rb +0 -95
  178. data/lib/swarm_sdk/workflow/builder.rb +0 -227
  179. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  180. data/lib/swarm_sdk/workflow/node_builder.rb +0 -593
  181. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -250
  182. data/lib/swarm_sdk/workflow.rb +0 -589
  183. data/lib/swarm_sdk.rb +0 -718
@@ -1,79 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Agent
5
- module ChatHelpers
6
- # System reminder collection and injection
7
- #
8
- # Extracted from Chat to reduce class size and centralize reminder logic.
9
- module SystemReminders
10
- # Collect reminders from all plugins
11
- #
12
- # @param prompt [String] User's message
13
- # @param is_first_message [Boolean] True if first message
14
- # @return [Array<String>] Array of reminder strings
15
- def collect_plugin_reminders(prompt, is_first_message:)
16
- return [] unless @agent_name
17
-
18
- PluginRegistry.all.flat_map do |plugin|
19
- plugin.on_user_message(
20
- agent_name: @agent_name,
21
- prompt: prompt,
22
- is_first_message: is_first_message,
23
- )
24
- end.compact
25
- end
26
-
27
- # Collect all system reminders for this message
28
- #
29
- # Returns an array of reminder strings that should be injected as ephemeral content.
30
- # These are sent to the LLM but not stored in message history.
31
- #
32
- # @param prompt [String] User prompt
33
- # @param is_first [Boolean] Whether this is the first message
34
- # @return [Array<String>] Array of reminder strings
35
- def collect_system_reminders(prompt, is_first)
36
- reminders = []
37
-
38
- if is_first
39
- # Add toolset reminder on first message
40
- reminders << build_toolset_reminder
41
-
42
- # Add todo list reminder if agent has TodoWrite tool
43
- reminders << SystemReminderInjector::AFTER_FIRST_MESSAGE_REMINDER if has_tool?(:TodoWrite)
44
-
45
- # Collect plugin reminders
46
- reminders.concat(collect_plugin_reminders(prompt, is_first_message: true))
47
- else
48
- # Add periodic TodoWrite reminder if needed
49
- if has_tool?(:TodoWrite) && SystemReminderInjector.should_inject_todowrite_reminder?(self, @last_todowrite_message_index)
50
- reminders << SystemReminderInjector::TODOWRITE_PERIODIC_REMINDER
51
- @last_todowrite_message_index = SystemReminderInjector.find_last_todowrite_index(self)
52
- end
53
-
54
- # Collect plugin reminders
55
- reminders.concat(collect_plugin_reminders(prompt, is_first_message: false))
56
- end
57
-
58
- reminders
59
- end
60
-
61
- private
62
-
63
- # Build toolset reminder listing all available tools
64
- #
65
- # @return [String] System reminder with tool list
66
- def build_toolset_reminder
67
- tools_list = tool_names
68
-
69
- reminder = "<system-reminder>\n"
70
- reminder += "Tools available: #{tools_list.join(", ")}\n\n"
71
- reminder += "Only use tools from this list. Do not attempt to use tools that are not listed here.\n"
72
- reminder += "</system-reminder>"
73
-
74
- reminder
75
- end
76
- end
77
- end
78
- end
79
- end
@@ -1,146 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- module Agent
5
- module ChatHelpers
6
- # Token usage tracking and context limit management
7
- #
8
- # Extracted from Chat to reduce class size and centralize token metrics.
9
- module TokenTracking
10
- # Get context window limit for the current model
11
- #
12
- # @return [Integer, nil] Maximum context tokens
13
- def context_limit
14
- return @explicit_context_window if @explicit_context_window
15
- return @real_model_info.context_window if @real_model_info&.context_window
16
-
17
- model_context_window
18
- rescue StandardError
19
- nil
20
- end
21
-
22
- # Calculate cumulative input tokens for the conversation
23
- #
24
- # Gets input_tokens from the most recent assistant message, which represents
25
- # the total context size sent to the model (not sum of all messages).
26
- #
27
- # @return [Integer] Total input tokens used
28
- def cumulative_input_tokens
29
- find_last_message { |msg| msg.role == :assistant && msg.input_tokens }&.input_tokens || 0
30
- end
31
-
32
- # Calculate cumulative output tokens across all assistant messages
33
- #
34
- # @return [Integer] Total output tokens used
35
- def cumulative_output_tokens
36
- assistant_messages.sum { |msg| msg.output_tokens || 0 }
37
- end
38
-
39
- # Calculate cumulative cached tokens
40
- #
41
- # @return [Integer] Total cached tokens used
42
- def cumulative_cached_tokens
43
- assistant_messages.sum { |msg| msg.cached_tokens || 0 }
44
- end
45
-
46
- # Calculate cumulative cache creation tokens
47
- #
48
- # @return [Integer] Total tokens written to cache
49
- def cumulative_cache_creation_tokens
50
- assistant_messages.sum { |msg| msg.cache_creation_tokens || 0 }
51
- end
52
-
53
- # Calculate effective input tokens (excluding cache hits)
54
- #
55
- # @return [Integer] Actual input tokens charged
56
- def effective_input_tokens
57
- cumulative_input_tokens - cumulative_cached_tokens
58
- end
59
-
60
- # Calculate total tokens used (input + output)
61
- #
62
- # @return [Integer] Total tokens used
63
- def cumulative_total_tokens
64
- cumulative_input_tokens + cumulative_output_tokens
65
- end
66
-
67
- # Calculate percentage of context window used
68
- #
69
- # @return [Float] Percentage (0.0 to 100.0)
70
- def context_usage_percentage
71
- limit = context_limit
72
- return 0.0 if limit.nil? || limit.zero?
73
-
74
- (cumulative_total_tokens.to_f / limit * 100).round(2)
75
- end
76
-
77
- # Calculate remaining tokens in context window
78
- #
79
- # @return [Integer, nil] Tokens remaining
80
- def tokens_remaining
81
- limit = context_limit
82
- return if limit.nil?
83
-
84
- limit - cumulative_total_tokens
85
- end
86
-
87
- # Calculate cumulative input cost based on tokens and model pricing
88
- #
89
- # @return [Float] Total input cost in dollars
90
- def cumulative_input_cost
91
- pricing = model_pricing
92
- return 0.0 unless pricing
93
-
94
- input_price = pricing["input_per_million"] || pricing[:input_per_million] || 0.0
95
- (cumulative_input_tokens / 1_000_000.0) * input_price
96
- end
97
-
98
- # Calculate cumulative output cost based on tokens and model pricing
99
- #
100
- # @return [Float] Total output cost in dollars
101
- def cumulative_output_cost
102
- pricing = model_pricing
103
- return 0.0 unless pricing
104
-
105
- output_price = pricing["output_per_million"] || pricing[:output_per_million] || 0.0
106
- (cumulative_output_tokens / 1_000_000.0) * output_price
107
- end
108
-
109
- # Calculate cumulative total cost (input + output)
110
- #
111
- # @return [Float] Total cost in dollars
112
- def cumulative_total_cost
113
- cumulative_input_cost + cumulative_output_cost
114
- end
115
-
116
- # Compact the conversation history to reduce token usage
117
- #
118
- # @param options [Hash] Compression options
119
- # @return [ContextCompactor::Metrics] Compression statistics
120
- def compact_context(**options)
121
- compactor = ContextCompactor.new(self, options)
122
- compactor.compact
123
- end
124
-
125
- private
126
-
127
- # Get pricing info for the current model
128
- #
129
- # Extracts standard text token pricing from model info.
130
- #
131
- # @return [Hash, nil] Pricing hash with input_per_million and output_per_million
132
- def model_pricing
133
- return unless @real_model_info&.pricing
134
-
135
- pricing = @real_model_info.pricing
136
- text_pricing = pricing["text_tokens"] || pricing[:text_tokens]
137
- return unless text_pricing
138
-
139
- text_pricing["standard"] || text_pricing[:standard]
140
- rescue StandardError
141
- nil
142
- end
143
- end
144
- end
145
- end
146
- end
@@ -1,115 +0,0 @@
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
- # NOTE: Compression threshold now accessed via SwarmSDK.config.context_compression_threshold
34
-
35
- attr_reader :name, :delegation_tools, :metadata, :warning_thresholds_hit, :swarm_id, :parent_swarm_id
36
-
37
- # Initialize a new agent context
38
- #
39
- # @param name [Symbol, String] Agent name
40
- # @param swarm_id [String] Swarm ID for event tracking
41
- # @param parent_swarm_id [String, nil] Parent swarm ID (nil for root swarms)
42
- # @param delegation_tools [Array<String>] Names of tools that are delegations
43
- # @param metadata [Hash] Optional metadata about the agent
44
- def initialize(name:, swarm_id:, parent_swarm_id: nil, delegation_tools: [], metadata: {})
45
- @name = name.to_sym
46
- @swarm_id = swarm_id
47
- @parent_swarm_id = parent_swarm_id
48
- @delegation_tools = Set.new(delegation_tools.map(&:to_s))
49
- @metadata = metadata
50
- @delegation_call_ids = Set.new
51
- @delegation_targets = {}
52
- @warning_thresholds_hit = Set.new
53
- end
54
-
55
- # Track a delegation tool call
56
- #
57
- # @param call_id [String] Tool call ID
58
- # @param target [String] Target agent/tool name
59
- # @return [void]
60
- def track_delegation(call_id:, target:)
61
- @delegation_call_ids.add(call_id)
62
- @delegation_targets[call_id] = target
63
- end
64
-
65
- # Check if a tool call is a delegation
66
- #
67
- # @param call_id [String] Tool call ID
68
- # @return [Boolean]
69
- def delegation?(call_id:)
70
- @delegation_call_ids.include?(call_id)
71
- end
72
-
73
- # Get the delegation target for a tool call
74
- #
75
- # @param call_id [String] Tool call ID
76
- # @return [String, nil] Target agent/tool name, or nil if not a delegation
77
- def delegation_target(call_id:)
78
- @delegation_targets[call_id]
79
- end
80
-
81
- # Remove a delegation from tracking (after it completes)
82
- #
83
- # @param call_id [String] Tool call ID
84
- # @return [void]
85
- def clear_delegation(call_id:)
86
- @delegation_targets.delete(call_id)
87
- @delegation_call_ids.delete(call_id)
88
- end
89
-
90
- # Check if a tool name is a delegation tool
91
- #
92
- # @param tool_name [String] Tool name
93
- # @return [Boolean]
94
- def delegation_tool?(tool_name)
95
- @delegation_tools.include?(tool_name.to_s)
96
- end
97
-
98
- # Record that a context warning threshold has been hit
99
- #
100
- # @param threshold [Integer] Threshold percentage (80, 90, etc)
101
- # @return [Boolean] true if this is the first time hitting this threshold
102
- def hit_warning_threshold?(threshold)
103
- !@warning_thresholds_hit.add?(threshold).nil?
104
- end
105
-
106
- # Check if a warning threshold has been hit
107
- #
108
- # @param threshold [Integer] Threshold percentage
109
- # @return [Boolean]
110
- def warning_threshold_hit?(threshold)
111
- @warning_thresholds_hit.include?(threshold)
112
- end
113
- end
114
- end
115
- end
@@ -1,315 +0,0 @@
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
- # Expose compression state for snapshot/restore
22
- # NOTE: @compression_applied initializes to nil (not false), only set to true when compression runs
23
- attr_reader :compression_applied
24
- attr_writer :compression_applied
25
-
26
- def initialize
27
- # Ephemeral content to append to messages for this turn only
28
- # Format: { message_index => [array of reminder strings] }
29
- @ephemeral_content = {}
30
- # NOTE: @compression_applied is NOT initialized here - starts as nil
31
- end
32
-
33
- # Track ephemeral content to append to a specific message
34
- #
35
- # Reminders will be embedded in the message content when sent to LLM,
36
- # but are NOT persisted in the message history.
37
- #
38
- # @param message_index [Integer] Index of message to append to
39
- # @param content [String] Reminder content to append
40
- # @return [void]
41
- def add_ephemeral_content_for_message(message_index, content)
42
- @ephemeral_content[message_index] ||= []
43
- @ephemeral_content[message_index] << content
44
- end
45
-
46
- # Add ephemeral reminder to the most recent message
47
- #
48
- # This will append the reminder to the last message in the array when
49
- # preparing for LLM, but won't modify the stored message.
50
- #
51
- # @param content [String] Reminder content
52
- # @param messages_array [Array<RubyLLM::Message>] Message array to get index from
53
- # @return [void]
54
- def add_ephemeral_reminder(content, messages_array:)
55
- message_index = messages_array.size - 1
56
- return if message_index < 0
57
-
58
- add_ephemeral_content_for_message(message_index, content)
59
- end
60
-
61
- # Prepare messages for LLM API call
62
- #
63
- # Embeds ephemeral content into messages for this turn only.
64
- # Does NOT modify the persistent messages array.
65
- #
66
- # @param persistent_messages [Array<RubyLLM::Message>] Messages from @messages
67
- # @return [Array<RubyLLM::Message>] Messages with ephemeral content embedded
68
- def prepare_for_llm(persistent_messages)
69
- return persistent_messages.dup if @ephemeral_content.empty?
70
-
71
- # Clone messages and embed ephemeral content
72
- messages_for_llm = persistent_messages.map.with_index do |msg, index|
73
- ephemeral_for_this_msg = @ephemeral_content[index]
74
-
75
- # No ephemeral content for this message - use as-is
76
- next msg unless ephemeral_for_this_msg&.any?
77
-
78
- # Embed ephemeral content in this message
79
- original_content = msg.content.is_a?(RubyLLM::Content) ? msg.content.text : msg.content.to_s
80
- embedded_content = [original_content, *ephemeral_for_this_msg].join("\n\n")
81
-
82
- # Create new message with embedded content
83
- if msg.content.is_a?(RubyLLM::Content)
84
- RubyLLM::Message.new(
85
- role: msg.role,
86
- content: RubyLLM::Content.new(embedded_content, msg.content.attachments),
87
- tool_call_id: msg.tool_call_id,
88
- )
89
- else
90
- RubyLLM::Message.new(
91
- role: msg.role,
92
- content: embedded_content,
93
- tool_call_id: msg.tool_call_id,
94
- )
95
- end
96
- end
97
-
98
- messages_for_llm
99
- end
100
-
101
- # Clear all ephemeral content
102
- #
103
- # Should be called after LLM response is received.
104
- #
105
- # @return [void]
106
- def clear_ephemeral
107
- @ephemeral_content.clear
108
- end
109
-
110
- # Check if there is pending ephemeral content
111
- #
112
- # @return [Boolean] True if ephemeral content exists
113
- def has_ephemeral?
114
- @ephemeral_content.any?
115
- end
116
-
117
- # Get count of messages with ephemeral content
118
- #
119
- # @return [Integer] Number of messages with ephemeral content attached
120
- def ephemeral_count
121
- @ephemeral_content.size
122
- end
123
-
124
- # Extract all <system-reminder> blocks from content
125
- #
126
- # @param content [String] Content to extract from
127
- # @return [Array<String>] Array of system reminder blocks
128
- def extract_system_reminders(content)
129
- return [] if content.nil? || content.empty?
130
-
131
- content.scan(SYSTEM_REMINDER_REGEX)
132
- end
133
-
134
- # Strip all <system-reminder> blocks from content
135
- #
136
- # Returns clean content without system reminders.
137
- #
138
- # @param content [String] Content to strip from
139
- # @return [String] Clean content
140
- def strip_system_reminders(content)
141
- return content if content.nil? || content.empty?
142
-
143
- content.gsub(SYSTEM_REMINDER_REGEX, "").strip
144
- end
145
-
146
- # Check if content contains system reminders
147
- #
148
- # @param content [String] Content to check
149
- # @return [Boolean] True if reminders found
150
- def has_system_reminders?(content)
151
- return false if content.nil? || content.empty?
152
-
153
- SYSTEM_REMINDER_REGEX.match?(content)
154
- end
155
-
156
- # ============================================================================
157
- # FUTURE: Context Optimization Methods (Hooks for Later Implementation)
158
- # ============================================================================
159
-
160
- # Future: Summarize old messages to save context window space
161
- #
162
- # @param messages [Array<RubyLLM::Message>] Messages to potentially summarize
163
- # @param before_index [Integer] Summarize messages before this index
164
- # @param strategy [Symbol] Summarization strategy (:llm, :truncate, :remove)
165
- # @return [Array<RubyLLM::Message>] Optimized message array
166
- def summarize_old_messages(messages, before_index:, strategy: :truncate)
167
- # TODO: Implement when needed
168
- messages
169
- end
170
-
171
- # Future: Truncate messages to fit within context window
172
- #
173
- # @param messages [Array<RubyLLM::Message>] Messages to fit
174
- # @param max_tokens [Integer] Maximum token budget
175
- # @param keep_recent [Integer] Number of recent messages to always keep
176
- # @return [Array<RubyLLM::Message>] Truncated messages
177
- def truncate_to_fit(messages, max_tokens:, keep_recent: 10)
178
- # TODO: Implement when needed
179
- messages
180
- end
181
-
182
- # Compress verbose tool results for older messages
183
- #
184
- # Uses progressive compression: older messages are compressed more aggressively.
185
- # Preserves user/assistant messages at full detail (conversational context).
186
- #
187
- # @param messages [Array<RubyLLM::Message>] Messages to compress
188
- # @param keep_recent [Integer] Number of recent messages to keep at full detail
189
- # @return [Array<RubyLLM::Message>] Compressed messages
190
- def compress_tool_results(messages, keep_recent: 10)
191
- messages.map.with_index do |msg, i|
192
- # Keep recent messages at full detail
193
- next msg if i >= messages.size - keep_recent
194
-
195
- # Keep user/assistant messages (conversational flow is important)
196
- next msg if [:user, :assistant].include?(msg.role)
197
-
198
- # Compress old tool results
199
- if msg.role == :tool
200
- compress_tool_message(msg, age: messages.size - i)
201
- else
202
- msg
203
- end
204
- end
205
- end
206
-
207
- # Compress a single tool message based on age
208
- #
209
- # Progressive compression: older messages get compressed more.
210
- # For re-runnable tools (Read, Grep, Glob, etc.), adds instruction to re-run if needed.
211
- #
212
- # @param msg [RubyLLM::Message] Tool message to compress
213
- # @param age [Integer] How many messages ago (higher = older)
214
- # @return [RubyLLM::Message] Compressed message
215
- def compress_tool_message(msg, age:)
216
- content = msg.content.to_s
217
-
218
- # Progressive compression based on age
219
- max_length = case age
220
- when 0..10 then return msg # Recent: keep full detail
221
- when 11..20 then 1000 # Medium age: light compression
222
- when 21..40 then 500 # Old: moderate compression
223
- when 41..60 then 200 # Very old: heavy compression
224
- else 100 # Ancient: minimal summary
225
- end
226
-
227
- return msg if content.length <= max_length
228
-
229
- # Compress while preserving structure
230
- compressed = content.slice(0, max_length)
231
- truncated_chars = content.length - max_length
232
- compressed += "\n...[#{truncated_chars} chars truncated for context management]"
233
-
234
- # Detect if this is a re-runnable tool and add helpful instruction
235
- tool_name = detect_tool_name(content)
236
- if rerunnable_tool?(tool_name)
237
- compressed += "\n\nšŸ’” If you need the full output, re-run the #{tool_name} tool with the same parameters."
238
- end
239
-
240
- RubyLLM::Message.new(
241
- role: :tool,
242
- content: compressed,
243
- tool_call_id: msg.tool_call_id,
244
- )
245
- end
246
-
247
- # Detect tool name from content
248
- #
249
- # @param content [String] Tool result content
250
- # @return [String, nil] Tool name or nil
251
- def detect_tool_name(content)
252
- # Many tool results start with patterns we can detect
253
- case content
254
- when /^\s*\d+→/ # Line numbers (Read, MemoryRead)
255
- content.include?("memory://") ? "MemoryRead" : "Read"
256
- when /^Memory entries matching/ # MemoryGlob
257
- "MemoryGlob"
258
- when /^Found \d+ files? matching/ # Glob
259
- "Glob"
260
- when /matches in \d+ files?|No matches found/ # Grep, MemoryGrep
261
- content.include?("memory://") ? "MemoryGrep" : "Grep"
262
- when %r{^Stored at memory://} # MemoryWrite (not re-runnable but identifiable)
263
- "MemoryWrite"
264
- when %r{^Deleted memory://} # MemoryDelete
265
- "MemoryDelete"
266
- end
267
- end
268
-
269
- # Check if a tool is re-runnable (idempotent, can get same data again)
270
- #
271
- # @param tool_name [String, nil] Tool name
272
- # @return [Boolean] True if tool can be re-run safely
273
- def rerunnable_tool?(tool_name)
274
- return false if tool_name.nil?
275
-
276
- # These tools are idempotent - re-running gives same/current data
277
- ["Read", "MemoryRead", "Grep", "MemoryGrep", "Glob", "MemoryGlob"].include?(tool_name)
278
- end
279
-
280
- # Automatically compress messages when context threshold is hit
281
- #
282
- # This is called automatically when context usage crosses 60% threshold.
283
- # Returns compressed messages array for immediate use.
284
- #
285
- # @param messages [Array<RubyLLM::Message>] Current message array
286
- # @param keep_recent [Integer] Number of recent messages to keep full
287
- # @return [Array<RubyLLM::Message>] Compressed messages
288
- def auto_compress_on_threshold(messages, keep_recent: 10)
289
- return messages if @compression_applied
290
-
291
- # Mark as applied to avoid compressing multiple times
292
- @compression_applied = true
293
-
294
- compress_tool_results(messages, keep_recent: keep_recent)
295
- end
296
-
297
- # Reset compression flag (when conversation is reset)
298
- #
299
- # @return [void]
300
- def reset_compression
301
- @compression_applied = false
302
- end
303
-
304
- # Future: Detect if context is becoming bloated
305
- #
306
- # @param messages [Array<RubyLLM::Message>] Messages to analyze
307
- # @param threshold [Float] Bloat threshold (0.0-1.0)
308
- # @return [Hash] Bloat analysis with recommendations
309
- def analyze_context_bloat(messages, threshold: 0.7)
310
- # TODO: Implement when needed
311
- { bloated: false, recommendations: [] }
312
- end
313
- end
314
- end
315
- end