swarm_memory 2.1.3 → 2.1.4
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.
- checksums.yaml +4 -4
- data/lib/claude_swarm/claude_mcp_server.rb +1 -0
- data/lib/claude_swarm/cli.rb +5 -18
- data/lib/claude_swarm/configuration.rb +2 -15
- data/lib/claude_swarm/mcp_generator.rb +1 -0
- data/lib/claude_swarm/openai/chat_completion.rb +4 -12
- data/lib/claude_swarm/openai/executor.rb +3 -1
- data/lib/claude_swarm/openai/responses.rb +13 -32
- data/lib/claude_swarm/version.rb +1 -1
- data/lib/swarm_cli/commands/run.rb +2 -2
- data/lib/swarm_cli/config_loader.rb +11 -11
- data/lib/swarm_cli/formatters/human_formatter.rb +70 -0
- data/lib/swarm_cli/interactive_repl.rb +11 -5
- data/lib/swarm_cli/ui/icons.rb +0 -23
- data/lib/swarm_cli/version.rb +1 -1
- data/lib/swarm_memory/adapters/filesystem_adapter.rb +11 -34
- data/lib/swarm_memory/integration/sdk_plugin.rb +87 -7
- data/lib/swarm_memory/version.rb +1 -1
- data/lib/swarm_memory.rb +1 -1
- data/lib/swarm_sdk/agent/builder.rb +58 -0
- data/lib/swarm_sdk/agent/chat.rb +527 -1059
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +9 -88
- data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +111 -44
- data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
- data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +233 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +1 -1
- data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +12 -12
- data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
- data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +98 -0
- data/lib/swarm_sdk/agent/context.rb +2 -2
- data/lib/swarm_sdk/agent/definition.rb +66 -154
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +4 -2
- data/lib/swarm_sdk/agent/system_prompt_builder.rb +161 -0
- data/lib/swarm_sdk/builders/base_builder.rb +409 -0
- data/lib/swarm_sdk/concerns/cleanupable.rb +39 -0
- data/lib/swarm_sdk/concerns/snapshotable.rb +67 -0
- data/lib/swarm_sdk/concerns/validatable.rb +55 -0
- data/lib/swarm_sdk/configuration/parser.rb +353 -0
- data/lib/swarm_sdk/configuration/translator.rb +255 -0
- data/lib/swarm_sdk/configuration.rb +65 -543
- data/lib/swarm_sdk/context_compactor/token_counter.rb +3 -3
- data/lib/swarm_sdk/context_compactor.rb +6 -11
- data/lib/swarm_sdk/context_management/builder.rb +128 -0
- data/lib/swarm_sdk/context_management/context.rb +328 -0
- data/lib/swarm_sdk/defaults.rb +196 -0
- data/lib/swarm_sdk/events_to_messages.rb +18 -0
- data/lib/swarm_sdk/hooks/shell_executor.rb +2 -1
- data/lib/swarm_sdk/log_collector.rb +179 -29
- data/lib/swarm_sdk/log_stream.rb +29 -0
- data/lib/swarm_sdk/node_context.rb +1 -1
- data/lib/swarm_sdk/observer/builder.rb +81 -0
- data/lib/swarm_sdk/observer/config.rb +45 -0
- data/lib/swarm_sdk/observer/manager.rb +236 -0
- data/lib/swarm_sdk/patterns/agent_observer.rb +160 -0
- data/lib/swarm_sdk/plugin.rb +93 -3
- data/lib/swarm_sdk/snapshot.rb +6 -6
- data/lib/swarm_sdk/snapshot_from_events.rb +13 -2
- data/lib/swarm_sdk/state_restorer.rb +136 -151
- data/lib/swarm_sdk/state_snapshot.rb +65 -100
- data/lib/swarm_sdk/swarm/agent_initializer.rb +180 -136
- data/lib/swarm_sdk/swarm/builder.rb +44 -578
- data/lib/swarm_sdk/swarm/executor.rb +213 -0
- data/lib/swarm_sdk/swarm/hook_triggers.rb +150 -0
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +340 -0
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
- data/lib/swarm_sdk/swarm/tool_configurator.rb +42 -138
- data/lib/swarm_sdk/swarm.rb +137 -679
- data/lib/swarm_sdk/tools/bash.rb +11 -3
- data/lib/swarm_sdk/tools/delegate.rb +61 -43
- data/lib/swarm_sdk/tools/edit.rb +8 -13
- data/lib/swarm_sdk/tools/glob.rb +9 -1
- data/lib/swarm_sdk/tools/grep.rb +7 -0
- data/lib/swarm_sdk/tools/multi_edit.rb +15 -11
- data/lib/swarm_sdk/tools/path_resolver.rb +51 -2
- data/lib/swarm_sdk/tools/read.rb +11 -13
- data/lib/swarm_sdk/tools/registry.rb +122 -10
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +8 -5
- data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
- data/lib/swarm_sdk/tools/todo_write.rb +7 -0
- data/lib/swarm_sdk/tools/web_fetch.rb +3 -2
- data/lib/swarm_sdk/tools/write.rb +8 -13
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk/{node → workflow}/agent_config.rb +1 -1
- data/lib/swarm_sdk/workflow/builder.rb +143 -0
- data/lib/swarm_sdk/workflow/executor.rb +497 -0
- data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +3 -3
- data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +3 -2
- data/lib/swarm_sdk/{node_orchestrator.rb → workflow.rb} +152 -456
- data/lib/swarm_sdk.rb +33 -3
- metadata +37 -14
- data/lib/swarm_memory/chat_extension.rb +0 -34
- data/lib/swarm_sdk/providers/openai_with_responses.rb +0 -589
|
@@ -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
|
|
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
|
-
|
|
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
|
-
#
|
|
326
|
-
#
|
|
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
|
-
|
|
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
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module SwarmSDK
|
|
4
|
+
# Centralized configuration defaults for SwarmSDK
|
|
5
|
+
#
|
|
6
|
+
# This module provides well-documented default values for all configurable
|
|
7
|
+
# aspects of the SDK. Values are organized by category and include explanations
|
|
8
|
+
# for their purpose and rationale.
|
|
9
|
+
#
|
|
10
|
+
# @example Accessing defaults
|
|
11
|
+
# SwarmSDK::Defaults::Timeouts::AGENT_REQUEST_SECONDS
|
|
12
|
+
# SwarmSDK::Defaults::Concurrency::GLOBAL_LIMIT
|
|
13
|
+
# SwarmSDK::Defaults::Limits::OUTPUT_CHARACTERS
|
|
14
|
+
module Defaults
|
|
15
|
+
# Concurrency limits for parallel execution
|
|
16
|
+
#
|
|
17
|
+
# These limits prevent overwhelming external services and ensure
|
|
18
|
+
# fair resource usage across the system.
|
|
19
|
+
module Concurrency
|
|
20
|
+
# Maximum concurrent API calls across entire swarm
|
|
21
|
+
#
|
|
22
|
+
# This limits total parallel LLM API requests to prevent rate limiting
|
|
23
|
+
# and excessive resource consumption. 50 is a balanced value that allows
|
|
24
|
+
# good parallelism while respecting API rate limits.
|
|
25
|
+
GLOBAL_LIMIT = 50
|
|
26
|
+
|
|
27
|
+
# Maximum parallel tool executions per agent
|
|
28
|
+
#
|
|
29
|
+
# Limits concurrent tool calls within a single agent. 10 allows
|
|
30
|
+
# meaningful parallelism (e.g., reading multiple files) without
|
|
31
|
+
# overwhelming the system.
|
|
32
|
+
LOCAL_LIMIT = 10
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Timeout values for various operations
|
|
36
|
+
#
|
|
37
|
+
# All timeouts are in seconds unless explicitly marked as milliseconds.
|
|
38
|
+
# Timeouts balance responsiveness with allowing enough time for operations
|
|
39
|
+
# to complete successfully.
|
|
40
|
+
module Timeouts
|
|
41
|
+
# LLM API request timeout (seconds)
|
|
42
|
+
#
|
|
43
|
+
# Default timeout for Claude/GPT API calls. 5 minutes accommodates
|
|
44
|
+
# reasoning models (o1, Claude with extended thinking) which can take
|
|
45
|
+
# longer to process complex queries.
|
|
46
|
+
AGENT_REQUEST_SECONDS = 300
|
|
47
|
+
|
|
48
|
+
# Bash command execution timeout (milliseconds)
|
|
49
|
+
#
|
|
50
|
+
# Default timeout for shell commands. 2 minutes balances allowing
|
|
51
|
+
# build/test commands while preventing runaway processes.
|
|
52
|
+
BASH_COMMAND_MS = 120_000
|
|
53
|
+
|
|
54
|
+
# Maximum Bash command timeout (milliseconds)
|
|
55
|
+
#
|
|
56
|
+
# Hard upper limit for bash commands. 10 minutes prevents indefinitely
|
|
57
|
+
# running commands while allowing long builds/tests.
|
|
58
|
+
BASH_COMMAND_MAX_MS = 600_000
|
|
59
|
+
|
|
60
|
+
# Web fetch timeout (seconds)
|
|
61
|
+
#
|
|
62
|
+
# Timeout for HTTP requests in WebFetch tool. 30 seconds is standard
|
|
63
|
+
# for web requests, allowing slow servers while timing out unresponsive ones.
|
|
64
|
+
WEB_FETCH_SECONDS = 30
|
|
65
|
+
|
|
66
|
+
# Shell hook executor timeout (seconds)
|
|
67
|
+
#
|
|
68
|
+
# Default timeout for hook shell commands. 60 seconds allows complex
|
|
69
|
+
# pre/post hooks while preventing indefinite blocking.
|
|
70
|
+
HOOK_SHELL_SECONDS = 60
|
|
71
|
+
|
|
72
|
+
# Workflow transformer command timeout (seconds)
|
|
73
|
+
#
|
|
74
|
+
# Timeout for input/output transformer bash commands. 60 seconds allows
|
|
75
|
+
# data transformation operations while preventing stalls.
|
|
76
|
+
TRANSFORMER_COMMAND_SECONDS = 60
|
|
77
|
+
|
|
78
|
+
# OpenAI responses API ID TTL (seconds)
|
|
79
|
+
#
|
|
80
|
+
# Time-to-live for cached response IDs. 5 minutes allows conversation
|
|
81
|
+
# continuity while preventing stale cache issues.
|
|
82
|
+
RESPONSES_API_TTL_SECONDS = 300
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Output and content size limits
|
|
86
|
+
#
|
|
87
|
+
# These limits prevent overwhelming context windows and ensure
|
|
88
|
+
# reasonable memory usage.
|
|
89
|
+
module Limits
|
|
90
|
+
# Maximum Bash output characters
|
|
91
|
+
#
|
|
92
|
+
# Truncates command output to prevent overwhelming agent context.
|
|
93
|
+
# 30,000 characters balances useful information with context constraints.
|
|
94
|
+
OUTPUT_CHARACTERS = 30_000
|
|
95
|
+
|
|
96
|
+
# Default lines to read from files
|
|
97
|
+
#
|
|
98
|
+
# When no explicit limit is set, Read tool returns first 2000 lines.
|
|
99
|
+
# This provides substantial file content while preventing huge files
|
|
100
|
+
# from overwhelming context.
|
|
101
|
+
READ_LINES = 2000
|
|
102
|
+
|
|
103
|
+
# Maximum characters per line in Read output
|
|
104
|
+
#
|
|
105
|
+
# Truncates very long lines to prevent single lines from consuming
|
|
106
|
+
# excessive context. 2000 characters per line is generous while
|
|
107
|
+
# protecting against minified files.
|
|
108
|
+
LINE_CHARACTERS = 2000
|
|
109
|
+
|
|
110
|
+
# Maximum WebFetch content length
|
|
111
|
+
#
|
|
112
|
+
# Limits web content fetched from URLs. 100,000 characters provides
|
|
113
|
+
# substantial page content while preventing huge pages from overwhelming context.
|
|
114
|
+
WEB_FETCH_CHARACTERS = 100_000
|
|
115
|
+
|
|
116
|
+
# Maximum Glob search results
|
|
117
|
+
#
|
|
118
|
+
# Limits number of file paths returned by Glob tool. 1000 results
|
|
119
|
+
# provides comprehensive search while preventing overwhelming output.
|
|
120
|
+
GLOB_RESULTS = 1000
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Storage limits for persistent data
|
|
124
|
+
module Storage
|
|
125
|
+
# Maximum size for single scratchpad entry (bytes)
|
|
126
|
+
#
|
|
127
|
+
# 3MB per entry prevents individual entries from consuming excessive storage
|
|
128
|
+
# while allowing substantial content (code, large texts).
|
|
129
|
+
ENTRY_SIZE_BYTES = 3_000_000
|
|
130
|
+
|
|
131
|
+
# Maximum total scratchpad storage (bytes)
|
|
132
|
+
#
|
|
133
|
+
# 100GB total storage provides ample room for extensive projects
|
|
134
|
+
# while preventing unbounded growth.
|
|
135
|
+
TOTAL_SIZE_BYTES = 100_000_000_000
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Context management settings
|
|
139
|
+
module Context
|
|
140
|
+
# Context usage percentage triggering compression warning
|
|
141
|
+
#
|
|
142
|
+
# When context usage reaches 60%, agents should consider compaction.
|
|
143
|
+
# This threshold provides buffer before hitting limits.
|
|
144
|
+
COMPRESSION_THRESHOLD_PERCENT = 60
|
|
145
|
+
|
|
146
|
+
# Message count between TodoWrite reminders
|
|
147
|
+
#
|
|
148
|
+
# After 8 messages without using TodoWrite, a gentle reminder is injected.
|
|
149
|
+
# Balances helpfulness without being annoying.
|
|
150
|
+
TODOWRITE_REMINDER_INTERVAL = 8
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# Token estimation factors
|
|
154
|
+
#
|
|
155
|
+
# Used for approximate token counting when precise counts aren't available.
|
|
156
|
+
module TokenEstimation
|
|
157
|
+
# Characters per token for prose text
|
|
158
|
+
#
|
|
159
|
+
# Average of ~4 characters per token for natural language text.
|
|
160
|
+
# Based on empirical analysis of tokenization patterns.
|
|
161
|
+
CHARS_PER_TOKEN_PROSE = 4.0
|
|
162
|
+
|
|
163
|
+
# Characters per token for code
|
|
164
|
+
#
|
|
165
|
+
# Code tends to have shorter tokens due to symbols and operators.
|
|
166
|
+
# ~3.5 characters per token accounts for this density.
|
|
167
|
+
CHARS_PER_TOKEN_CODE = 3.5
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Logging configuration
|
|
171
|
+
module Logging
|
|
172
|
+
# Default MCP client log level
|
|
173
|
+
#
|
|
174
|
+
# WARN level suppresses verbose MCP client logs while still
|
|
175
|
+
# reporting important issues.
|
|
176
|
+
MCP_LOG_LEVEL = Logger::WARN
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Agent configuration defaults
|
|
180
|
+
#
|
|
181
|
+
# Default values for agent configuration when not explicitly specified.
|
|
182
|
+
module Agent
|
|
183
|
+
# Default LLM model identifier
|
|
184
|
+
#
|
|
185
|
+
# OpenAI's GPT-5 is used as the default model. This can be overridden
|
|
186
|
+
# per-agent or globally via all_agents configuration.
|
|
187
|
+
MODEL = "gpt-5"
|
|
188
|
+
|
|
189
|
+
# Default LLM provider
|
|
190
|
+
#
|
|
191
|
+
# OpenAI is the default provider. Supported providers include:
|
|
192
|
+
# openai, anthropic, gemini, deepseek, openrouter, bedrock, etc.
|
|
193
|
+
PROVIDER = "openai"
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
end
|