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.
- checksums.yaml +4 -4
- data/{CHANGELOG.md → CHANGELOG.claude-swarm.md} +10 -0
- data/CLAUDE.md +346 -191
- data/decisions/2025-11-22-001-global-agent-registry.md +172 -0
- data/docs/v2/CHANGELOG.swarm_cli.md +20 -0
- data/docs/v2/CHANGELOG.swarm_memory.md +146 -1
- data/docs/v2/CHANGELOG.swarm_sdk.md +433 -10
- data/docs/v2/README.md +20 -5
- data/docs/v2/guides/complete-tutorial.md +95 -9
- data/docs/v2/guides/getting-started.md +10 -8
- data/docs/v2/guides/memory-adapters.md +41 -0
- data/docs/v2/guides/migrating-to-2.x.md +746 -0
- data/docs/v2/guides/plugins.md +52 -5
- data/docs/v2/guides/rails-integration.md +6 -0
- data/docs/v2/guides/snapshots.md +14 -14
- data/docs/v2/guides/swarm-memory.md +2 -13
- data/docs/v2/reference/architecture-flow.md +3 -3
- data/docs/v2/reference/cli.md +0 -1
- data/docs/v2/reference/configuration_reference.md +300 -0
- data/docs/v2/reference/event_payload_structures.md +27 -5
- data/docs/v2/reference/ruby-dsl.md +614 -18
- data/docs/v2/reference/swarm_memory_technical_details.md +7 -29
- data/docs/v2/reference/yaml.md +172 -54
- data/examples/snapshot_demo.rb +2 -2
- data/lib/claude_swarm/mcp_generator.rb +8 -21
- data/lib/claude_swarm/orchestrator.rb +8 -1
- 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 +0 -33
- data/lib/swarm_cli/interactive_repl.rb +2 -2
- 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/core/semantic_index.rb +10 -2
- data/lib/swarm_memory/core/storage.rb +7 -2
- data/lib/swarm_memory/dsl/memory_config.rb +37 -0
- data/lib/swarm_memory/integration/sdk_plugin.rb +201 -28
- data/lib/swarm_memory/optimization/defragmenter.rb +1 -1
- data/lib/swarm_memory/prompts/memory_researcher.md.erb +0 -1
- data/lib/swarm_memory/tools/load_skill.rb +0 -1
- data/lib/swarm_memory/tools/memory_edit.rb +2 -1
- data/lib/swarm_memory/tools/memory_read.rb +1 -1
- data/lib/swarm_memory/version.rb +1 -1
- data/lib/swarm_memory.rb +8 -6
- data/lib/swarm_sdk/agent/builder.rb +58 -0
- data/lib/swarm_sdk/agent/chat.rb +527 -1061
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +13 -88
- data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +108 -46
- data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
- data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +267 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +3 -3
- data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +11 -13
- data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
- data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +146 -0
- data/lib/swarm_sdk/agent/context.rb +1 -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/agent_registry.rb +146 -0
- data/lib/swarm_sdk/builders/base_builder.rb +488 -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/config.rb +302 -0
- data/lib/swarm_sdk/configuration/parser.rb +373 -0
- data/lib/swarm_sdk/configuration/translator.rb +255 -0
- data/lib/swarm_sdk/configuration.rb +77 -546
- data/lib/swarm_sdk/context_compactor/token_counter.rb +2 -6
- 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/custom_tool_registry.rb +226 -0
- data/lib/swarm_sdk/defaults.rb +196 -0
- data/lib/swarm_sdk/events_to_messages.rb +18 -0
- data/lib/swarm_sdk/hooks/adapter.rb +3 -3
- data/lib/swarm_sdk/hooks/shell_executor.rb +4 -2
- data/lib/swarm_sdk/log_collector.rb +179 -29
- data/lib/swarm_sdk/log_stream.rb +29 -0
- data/lib/swarm_sdk/models.json +4333 -1
- data/lib/swarm_sdk/models.rb +43 -2
- 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 +95 -5
- data/lib/swarm_sdk/result.rb +52 -0
- 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 +181 -137
- 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 +151 -0
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +341 -0
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
- data/lib/swarm_sdk/swarm/tool_configurator.rb +58 -140
- data/lib/swarm_sdk/swarm.rb +203 -683
- data/lib/swarm_sdk/tools/bash.rb +14 -8
- 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 +12 -4
- 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 +16 -18
- data/lib/swarm_sdk/tools/registry.rb +122 -10
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +9 -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 +20 -17
- 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 +192 -0
- data/lib/swarm_sdk/workflow/executor.rb +497 -0
- data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +7 -5
- data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +5 -3
- data/lib/swarm_sdk/{node_orchestrator.rb → workflow.rb} +152 -456
- data/lib/swarm_sdk.rb +294 -108
- data/rubocop/cop/security/no_reflection_methods.rb +1 -1
- data/swarm_cli.gemspec +1 -1
- data/swarm_memory.gemspec +8 -3
- data/swarm_sdk.gemspec +6 -4
- data/team_full.yml +124 -320
- metadata +42 -14
- data/lib/swarm_memory/chat_extension.rb +0 -34
- data/lib/swarm_memory/tools/memory_multi_edit.rb +0 -281
- data/lib/swarm_sdk/providers/openai_with_responses.rb +0 -589
- /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
|
-
|
|
77
|
+
SwarmSDK.config.chars_per_token_code # Code
|
|
82
78
|
else
|
|
83
|
-
|
|
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
|
|
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
|