claude_swarm 1.0.8 → 1.0.10

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 (114) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/CLAUDE.md +347 -191
  4. data/docs/v1/README.md +10 -0
  5. data/docs/v2/CHANGELOG.swarm_cli.md +8 -0
  6. data/docs/v2/CHANGELOG.swarm_memory.md +7 -1
  7. data/docs/v2/CHANGELOG.swarm_sdk.md +184 -9
  8. data/docs/v2/README.md +6 -1
  9. data/docs/v2/guides/complete-tutorial.md +2 -2
  10. data/docs/v2/guides/getting-started.md +7 -7
  11. data/docs/v2/guides/migrating-to-2.3.md +541 -0
  12. data/docs/v2/guides/snapshots.md +14 -14
  13. data/docs/v2/reference/architecture-flow.md +3 -3
  14. data/docs/v2/reference/event_payload_structures.md +1 -1
  15. data/docs/v2/reference/ruby-dsl.md +157 -14
  16. data/docs/v2/reference/yaml.md +170 -52
  17. data/examples/snapshot_demo.rb +2 -2
  18. data/lib/claude_swarm/claude_mcp_server.rb +1 -0
  19. data/lib/claude_swarm/cli.rb +5 -0
  20. data/lib/claude_swarm/configuration.rb +2 -1
  21. data/lib/claude_swarm/mcp_generator.rb +8 -20
  22. data/lib/claude_swarm/openai/chat_completion.rb +2 -1
  23. data/lib/claude_swarm/openai/executor.rb +3 -1
  24. data/lib/claude_swarm/openai/responses.rb +11 -21
  25. data/lib/claude_swarm/version.rb +1 -1
  26. data/lib/swarm_cli/commands/run.rb +2 -2
  27. data/lib/swarm_cli/config_loader.rb +11 -11
  28. data/lib/swarm_cli/formatters/human_formatter.rb +0 -33
  29. data/lib/swarm_cli/interactive_repl.rb +2 -2
  30. data/lib/swarm_cli/ui/icons.rb +0 -23
  31. data/lib/swarm_cli/version.rb +1 -1
  32. data/lib/swarm_memory/adapters/filesystem_adapter.rb +11 -34
  33. data/lib/swarm_memory/integration/sdk_plugin.rb +87 -7
  34. data/lib/swarm_memory/version.rb +1 -1
  35. data/lib/swarm_memory.rb +1 -1
  36. data/lib/swarm_sdk/agent/builder.rb +58 -0
  37. data/lib/swarm_sdk/agent/chat.rb +527 -1061
  38. data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +9 -88
  39. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
  40. data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +108 -46
  41. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
  42. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +233 -0
  43. data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +1 -1
  44. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
  45. data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +12 -12
  46. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
  47. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +98 -0
  48. data/lib/swarm_sdk/agent/context.rb +2 -2
  49. data/lib/swarm_sdk/agent/definition.rb +66 -154
  50. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +4 -2
  51. data/lib/swarm_sdk/agent/system_prompt_builder.rb +161 -0
  52. data/lib/swarm_sdk/builders/base_builder.rb +409 -0
  53. data/lib/swarm_sdk/concerns/cleanupable.rb +39 -0
  54. data/lib/swarm_sdk/concerns/snapshotable.rb +67 -0
  55. data/lib/swarm_sdk/concerns/validatable.rb +55 -0
  56. data/lib/swarm_sdk/configuration/parser.rb +353 -0
  57. data/lib/swarm_sdk/configuration/translator.rb +255 -0
  58. data/lib/swarm_sdk/configuration.rb +65 -543
  59. data/lib/swarm_sdk/context_compactor/token_counter.rb +3 -3
  60. data/lib/swarm_sdk/context_compactor.rb +6 -11
  61. data/lib/swarm_sdk/context_management/builder.rb +128 -0
  62. data/lib/swarm_sdk/context_management/context.rb +328 -0
  63. data/lib/swarm_sdk/defaults.rb +196 -0
  64. data/lib/swarm_sdk/events_to_messages.rb +18 -0
  65. data/lib/swarm_sdk/hooks/shell_executor.rb +2 -1
  66. data/lib/swarm_sdk/log_collector.rb +179 -29
  67. data/lib/swarm_sdk/log_stream.rb +29 -0
  68. data/lib/swarm_sdk/node_context.rb +1 -1
  69. data/lib/swarm_sdk/observer/builder.rb +81 -0
  70. data/lib/swarm_sdk/observer/config.rb +45 -0
  71. data/lib/swarm_sdk/observer/manager.rb +236 -0
  72. data/lib/swarm_sdk/patterns/agent_observer.rb +160 -0
  73. data/lib/swarm_sdk/plugin.rb +93 -3
  74. data/lib/swarm_sdk/snapshot.rb +6 -6
  75. data/lib/swarm_sdk/snapshot_from_events.rb +13 -2
  76. data/lib/swarm_sdk/state_restorer.rb +136 -151
  77. data/lib/swarm_sdk/state_snapshot.rb +65 -100
  78. data/lib/swarm_sdk/swarm/agent_initializer.rb +180 -136
  79. data/lib/swarm_sdk/swarm/builder.rb +44 -578
  80. data/lib/swarm_sdk/swarm/executor.rb +213 -0
  81. data/lib/swarm_sdk/swarm/hook_triggers.rb +150 -0
  82. data/lib/swarm_sdk/swarm/logging_callbacks.rb +340 -0
  83. data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
  84. data/lib/swarm_sdk/swarm/tool_configurator.rb +42 -138
  85. data/lib/swarm_sdk/swarm.rb +137 -680
  86. data/lib/swarm_sdk/tools/bash.rb +11 -3
  87. data/lib/swarm_sdk/tools/delegate.rb +61 -43
  88. data/lib/swarm_sdk/tools/edit.rb +8 -13
  89. data/lib/swarm_sdk/tools/glob.rb +9 -1
  90. data/lib/swarm_sdk/tools/grep.rb +7 -0
  91. data/lib/swarm_sdk/tools/multi_edit.rb +15 -11
  92. data/lib/swarm_sdk/tools/path_resolver.rb +51 -2
  93. data/lib/swarm_sdk/tools/read.rb +11 -13
  94. data/lib/swarm_sdk/tools/registry.rb +122 -10
  95. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +8 -5
  96. data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
  97. data/lib/swarm_sdk/tools/todo_write.rb +7 -0
  98. data/lib/swarm_sdk/tools/web_fetch.rb +3 -2
  99. data/lib/swarm_sdk/tools/write.rb +8 -13
  100. data/lib/swarm_sdk/version.rb +1 -1
  101. data/lib/swarm_sdk/{node → workflow}/agent_config.rb +1 -1
  102. data/lib/swarm_sdk/workflow/builder.rb +143 -0
  103. data/lib/swarm_sdk/workflow/executor.rb +497 -0
  104. data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +3 -3
  105. data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +3 -2
  106. data/lib/swarm_sdk/{node_orchestrator.rb → workflow.rb} +152 -456
  107. data/lib/swarm_sdk.rb +33 -3
  108. data/rubocop/cop/security/no_reflection_methods.rb +1 -1
  109. data/swarm_memory.gemspec +1 -1
  110. data/swarm_sdk.gemspec +4 -2
  111. data/team_full.yml +24 -24
  112. metadata +35 -11
  113. data/lib/swarm_memory/chat_extension.rb +0 -34
  114. data/lib/swarm_sdk/providers/openai_with_responses.rb +0 -589
@@ -2,6 +2,12 @@
2
2
 
3
3
  module ClaudeSwarm
4
4
  class McpGenerator
5
+ CLAUDE_MCP_SERVER_CONFIG = {
6
+ "type" => "stdio",
7
+ "command" => "claude",
8
+ "args" => ["mcp", "serve"],
9
+ }.freeze
10
+
5
11
  def initialize(configuration, vibe: false, restore_session_path: nil)
6
12
  @config = configuration
7
13
  @vibe = vibe
@@ -65,7 +71,7 @@ module ClaudeSwarm
65
71
  end
66
72
 
67
73
  # Add Claude tools MCP server for OpenAI instances
68
- mcp_servers["claude_tools"] = build_claude_tools_mcp_config if instance[:provider] == "openai"
74
+ mcp_servers["claude_tools"] = CLAUDE_MCP_SERVER_CONFIG.dup if instance[:provider] == "openai"
69
75
 
70
76
  config = {
71
77
  "instance_id" => @instance_ids[name],
@@ -96,25 +102,6 @@ module ClaudeSwarm
96
102
  end
97
103
  end
98
104
 
99
- def build_claude_tools_mcp_config
100
- # Build environment for claude mcp serve by excluding Ruby/Bundler-specific variables
101
- # This preserves all system variables while removing Ruby contamination
102
- clean_env = ENV.to_h.reject do |key, _|
103
- key.start_with?("BUNDLE_") ||
104
- key.start_with?("RUBY") ||
105
- key.start_with?("GEM_") ||
106
- key == "RUBYOPT" ||
107
- key == "RUBYLIB"
108
- end
109
-
110
- {
111
- "type" => "stdio",
112
- "command" => "claude",
113
- "args" => ["mcp", "serve"],
114
- "env" => clean_env,
115
- }
116
- end
117
-
118
105
  def build_instance_mcp_config(name, instance, calling_instance:, calling_instance_id:)
119
106
  # Get the path to the claude-swarm executable
120
107
  exe_path = "claude-swarm"
@@ -174,6 +161,7 @@ module ClaudeSwarm
174
161
  args.push("--api-version", instance[:api_version]) if instance[:api_version]
175
162
  args.push("--openai-token-env", instance[:openai_token_env]) if instance[:openai_token_env]
176
163
  args.push("--base-url", instance[:base_url]) if instance[:base_url]
164
+ args.push("--zdr", instance[:zdr].to_s) if instance.key?(:zdr)
177
165
  end
178
166
  end
179
167
 
@@ -5,7 +5,7 @@ module ClaudeSwarm
5
5
  class ChatCompletion
6
6
  MAX_TURNS_WITH_TOOLS = 100_000 # virtually infinite
7
7
 
8
- def initialize(openai_client:, mcp_client:, available_tools:, executor:, instance_name:, model:, temperature: nil, reasoning_effort: nil)
8
+ def initialize(openai_client:, mcp_client:, available_tools:, executor:, instance_name:, model:, temperature: nil, reasoning_effort: nil, zdr: false)
9
9
  @openai_client = openai_client
10
10
  @mcp_client = mcp_client
11
11
  @available_tools = available_tools
@@ -14,6 +14,7 @@ module ClaudeSwarm
14
14
  @model = model
15
15
  @temperature = temperature
16
16
  @reasoning_effort = reasoning_effort
17
+ @zdr = zdr # Not used in chat_completion API, but kept for compatibility
17
18
  @conversation_messages = []
18
19
  end
19
20
 
@@ -31,7 +31,7 @@ module ClaudeSwarm
31
31
  instance_name: nil, instance_id: nil, calling_instance: nil, calling_instance_id: nil,
32
32
  claude_session_id: nil, additional_directories: [], debug: false,
33
33
  temperature: nil, api_version: "chat_completion", openai_token_env: "OPENAI_API_KEY",
34
- base_url: nil, reasoning_effort: nil)
34
+ base_url: nil, reasoning_effort: nil, zdr: false)
35
35
  # Call parent initializer for common attributes
36
36
  super(
37
37
  working_directory: working_directory,
@@ -52,6 +52,7 @@ module ClaudeSwarm
52
52
  @api_version = api_version
53
53
  @base_url = base_url
54
54
  @reasoning_effort = reasoning_effort
55
+ @zdr = zdr
55
56
 
56
57
  # Conversation state for maintaining context
57
58
  @conversation_messages = []
@@ -162,6 +163,7 @@ module ClaudeSwarm
162
163
  model: @model,
163
164
  temperature: @temperature,
164
165
  reasoning_effort: @reasoning_effort,
166
+ zdr: @zdr,
165
167
  }
166
168
 
167
169
  if @api_version == "responses"
@@ -5,7 +5,7 @@ module ClaudeSwarm
5
5
  class Responses
6
6
  MAX_TURNS_WITH_TOOLS = 100_000 # virtually infinite
7
7
 
8
- def initialize(openai_client:, mcp_client:, available_tools:, executor:, instance_name:, model:, temperature: nil, reasoning_effort: nil)
8
+ def initialize(openai_client:, mcp_client:, available_tools:, executor:, instance_name:, model:, temperature: nil, reasoning_effort: nil, zdr: false)
9
9
  @openai_client = openai_client
10
10
  @mcp_client = mcp_client
11
11
  @available_tools = available_tools
@@ -14,6 +14,7 @@ module ClaudeSwarm
14
14
  @model = model
15
15
  @temperature = temperature
16
16
  @reasoning_effort = reasoning_effort
17
+ @zdr = zdr
17
18
  @system_prompt = nil
18
19
  end
19
20
 
@@ -58,6 +59,7 @@ module ClaudeSwarm
58
59
  else
59
60
  input
60
61
  end
62
+ conversation_array << { role: "user", content: parameters[:input] }
61
63
  else
62
64
  # Follow-up call with conversation array (function calls + outputs)
63
65
  parameters[:input] = conversation_array
@@ -70,8 +72,8 @@ module ClaudeSwarm
70
72
  @executor.logger.info { "Conversation item IDs: #{conversation_ids.inspect}" }
71
73
  end
72
74
 
73
- # Add previous response ID for conversation continuity
74
- parameters[:previous_response_id] = previous_response_id if previous_response_id
75
+ # Add previous response ID for conversation continuity (unless zdr is enabled)
76
+ parameters[:previous_response_id] = @zdr ? nil : previous_response_id
75
77
 
76
78
  # Add tools if available
77
79
  if @available_tools&.any?
@@ -106,7 +108,7 @@ module ClaudeSwarm
106
108
  @executor.logger.error { "Request parameters: #{JsonHandler.pretty_generate!(parameters)}" }
107
109
 
108
110
  # Try to extract and log the response body for better debugging
109
- if e.respond_to?(:response)
111
+ if e.respond_to?(:response) && e.response
110
112
  begin
111
113
  error_body = e.response[:body]
112
114
  @executor.logger.error { "Error response body: #{error_body}" }
@@ -122,7 +124,7 @@ module ClaudeSwarm
122
124
  error: {
123
125
  class: e.class.to_s,
124
126
  message: e.message,
125
- response_body: e.respond_to?(:response) ? e.response[:body] : nil,
127
+ response_body: e.respond_to?(:response) && e.response ? e.response[:body] : nil,
126
128
  backtrace: e.backtrace.first(5),
127
129
  },
128
130
  })
@@ -146,33 +148,21 @@ module ClaudeSwarm
146
148
 
147
149
  # Handle response based on output structure
148
150
  output = response["output"]
149
-
150
151
  if output.nil?
151
152
  @executor.logger.error { "No output in response" }
152
153
  return "Error: No output in OpenAI response"
153
154
  end
154
155
 
155
156
  # Check if output is an array (as per documentation)
156
- if output.is_a?(Array) && !output.empty?
157
+ if output.is_a?(Array) && output.any?
158
+ new_conversation = conversation_array.dup
159
+ new_conversation.concat(output)
157
160
  # Check if there are function calls
158
161
  function_calls = output.select { |item| item["type"] == "function_call" }
159
-
160
162
  if function_calls.any?
161
- # Check if we already have a conversation going
162
- if conversation_array.empty?
163
- # First depth - build new conversation
164
- new_conversation = build_conversation_with_outputs(function_calls)
165
- else
166
- # Subsequent depth - append to existing conversation
167
- # Don't re-add function calls, just add the new ones and their outputs
168
- new_conversation = conversation_array.dup
169
- append_new_outputs(function_calls, new_conversation)
170
- end
171
-
172
- # Recursively process with updated conversation
163
+ append_new_outputs(function_calls, new_conversation)
173
164
  process_responses_api(nil, new_conversation, response_id, depth + 1)
174
165
  else
175
- # Look for text response
176
166
  extract_text_response(output)
177
167
  end
178
168
  else
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ClaudeSwarm
4
- VERSION = "1.0.8"
4
+ VERSION = "1.0.10"
5
5
  end
@@ -134,8 +134,8 @@ module SwarmCLI
134
134
 
135
135
  def emit_validation_warnings(swarm, formatter)
136
136
  # Setup temporary logging to capture and emit warnings
137
- SwarmSDK::LogCollector.on_log do |log_entry|
138
- formatter.on_log(log_entry) if log_entry[:type] == "model_lookup_warning"
137
+ SwarmSDK::LogCollector.subscribe(filter: { type: "model_lookup_warning" }) do |log_entry|
138
+ formatter.on_log(log_entry)
139
139
  end
140
140
 
141
141
  SwarmSDK::LogStream.emitter = SwarmSDK::LogCollector
@@ -5,7 +5,7 @@ module SwarmCLI
5
5
  #
6
6
  # Supports:
7
7
  # - YAML files (.yml, .yaml) - loaded via SwarmSDK.load_file
8
- # - Ruby DSL files (.rb) - executed and expected to return a SwarmSDK::Swarm or SwarmSDK::NodeOrchestrator instance
8
+ # - Ruby DSL files (.rb) - executed and expected to return a SwarmSDK::Swarm or SwarmSDK::Workflow instance
9
9
  #
10
10
  # @example Load YAML config
11
11
  # swarm = ConfigLoader.load("config.yml")
@@ -19,10 +19,10 @@ module SwarmCLI
19
19
  #
20
20
  # Detects file type by extension:
21
21
  # - .yml, .yaml -> Load as YAML using SwarmSDK.load_file
22
- # - .rb -> Execute as Ruby DSL and expect SwarmSDK::Swarm or SwarmSDK::NodeOrchestrator instance
22
+ # - .rb -> Execute as Ruby DSL and expect SwarmSDK::Swarm or SwarmSDK::Workflow instance
23
23
  #
24
24
  # @param path [String, Pathname] Path to configuration file
25
- # @return [SwarmSDK::Swarm, SwarmSDK::NodeOrchestrator] Configured swarm or orchestrator instance
25
+ # @return [SwarmSDK::Swarm, SwarmSDK::Workflow] Configured swarm or workflow instance
26
26
  # @raise [SwarmCLI::ConfigurationError] If file not found or invalid format
27
27
  def load(path)
28
28
  path = Pathname.new(path).expand_path
@@ -59,27 +59,27 @@ module SwarmCLI
59
59
  # Load Ruby DSL configuration file
60
60
  #
61
61
  # Executes the Ruby file in a clean binding and expects it to return
62
- # a SwarmSDK::Swarm or SwarmSDK::NodeOrchestrator instance. The file should
63
- # use SwarmSDK.build or create a Swarm/NodeOrchestrator instance directly.
62
+ # a SwarmSDK::Swarm or SwarmSDK::Workflow instance. The file should
63
+ # use SwarmSDK.build or SwarmSDK.workflow or create a Swarm/Workflow instance directly.
64
64
  #
65
65
  # @param path [Pathname] Path to Ruby DSL file
66
- # @return [SwarmSDK::Swarm, SwarmSDK::NodeOrchestrator] Configured swarm or orchestrator instance
66
+ # @return [SwarmSDK::Swarm, SwarmSDK::Workflow] Configured swarm or workflow instance
67
67
  # @raise [ConfigurationError] If file doesn't return a valid instance
68
68
  def load_ruby_dsl(path)
69
69
  # Read the file content
70
70
  content = path.read
71
71
 
72
72
  # Execute in a clean binding with SwarmSDK available
73
- # This allows the DSL file to use SwarmSDK.build directly
73
+ # This allows the DSL file to use SwarmSDK.build or SwarmSDK.workflow directly
74
74
  result = eval(content, binding, path.to_s, 1) # rubocop:disable Security/Eval
75
75
 
76
- # Validate result is a Swarm or NodeOrchestrator instance
76
+ # Validate result is a Swarm or Workflow instance
77
77
  # Both have the same execute(prompt) interface
78
- unless result.is_a?(SwarmSDK::Swarm) || result.is_a?(SwarmSDK::NodeOrchestrator)
78
+ unless result.is_a?(SwarmSDK::Swarm) || result.is_a?(SwarmSDK::Workflow)
79
79
  raise ConfigurationError,
80
- "Ruby DSL file must return a SwarmSDK::Swarm or SwarmSDK::NodeOrchestrator instance. " \
80
+ "Ruby DSL file must return a SwarmSDK::Swarm or SwarmSDK::Workflow instance. " \
81
81
  "Got: #{result.class}. " \
82
- "Use: SwarmSDK.build { ... } or Swarm.new(...)"
82
+ "Use: SwarmSDK.build { ... } or SwarmSDK.workflow { ... }"
83
83
  end
84
84
 
85
85
  result
@@ -99,8 +99,6 @@ module SwarmCLI
99
99
  handle_llm_retry_attempt(entry)
100
100
  when "llm_retry_exhausted"
101
101
  handle_llm_retry_exhausted(entry)
102
- when "response_parse_error"
103
- handle_response_parse_error(entry)
104
102
  end
105
103
  end
106
104
 
@@ -647,37 +645,6 @@ module SwarmCLI
647
645
  )
648
646
  end
649
647
 
650
- def handle_response_parse_error(entry)
651
- agent = entry[:agent]
652
- error_class = entry[:error_class]
653
- error_message = entry[:error_message]
654
-
655
- # Stop agent thinking spinner (if active)
656
- unless @quiet
657
- spinner_key = "agent_#{agent}".to_sym
658
- @spinner_manager.stop(spinner_key) if @spinner_manager.active?(spinner_key)
659
- end
660
-
661
- lines = [
662
- @pastel.red("Failed to parse LLM API response"),
663
- @pastel.dim("Error: #{error_class}: #{error_message}"),
664
- ]
665
-
666
- # Add response body preview if available (truncated)
667
- if entry[:response_body]
668
- body_preview = entry[:response_body].to_s[0..200]
669
- body_preview += "..." if entry[:response_body].to_s.length > 200
670
- lines << @pastel.dim("Response: #{body_preview}")
671
- end
672
-
673
- @output.puts @panel.render(
674
- type: :error,
675
- title: "PARSE ERROR #{@agent_badge.render(agent)}",
676
- lines: lines,
677
- indent: @depth_tracker.get(agent),
678
- )
679
- end
680
-
681
648
  def display_todo_list(agent, timestamp)
682
649
  todos = SwarmSDK::Tools::Stores::TodoManager.get_todos(agent.to_sym)
683
650
  indent = @depth_tracker.indent(agent)
@@ -534,7 +534,7 @@ module SwarmCLI
534
534
  lead = @swarm.agent(@swarm.lead_agent)
535
535
 
536
536
  # Clear the agent's conversation history
537
- lead.reset_messages!
537
+ lead.replace_messages([])
538
538
 
539
539
  # Clear REPL conversation history
540
540
  @conversation_history.clear
@@ -575,7 +575,7 @@ module SwarmCLI
575
575
  case tool_name
576
576
  when /^Memory/, "LoadSkill"
577
577
  memory_tools << tool_name
578
- when /^DelegateTaskTo/
578
+ when /^WorkWith/
579
579
  delegation_tools << tool_name
580
580
  when /^mcp__/
581
581
  mcp_tools << tool_name
@@ -31,29 +31,6 @@ module SwarmCLI
31
31
  ARROW_RIGHT = "→"
32
32
  BULLET = "•"
33
33
  COMPRESS = "🗜️"
34
-
35
- # All icons as hash for backward compatibility
36
- ALL = {
37
- thinking: THINKING,
38
- response: RESPONSE,
39
- success: SUCCESS,
40
- error: ERROR,
41
- info: INFO,
42
- warning: WARNING,
43
- agent: AGENT,
44
- tool: TOOL,
45
- delegate: DELEGATE,
46
- result: RESULT,
47
- hook: HOOK,
48
- llm: LLM,
49
- tokens: TOKENS,
50
- cost: COST,
51
- time: TIME,
52
- sparkles: SPARKLES,
53
- arrow_right: ARROW_RIGHT,
54
- bullet: BULLET,
55
- compress: COMPRESS,
56
- }.freeze
57
34
  end
58
35
  end
59
36
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwarmCLI
4
- VERSION = "2.1.3"
4
+ VERSION = "2.1.4"
5
5
  end
@@ -93,10 +93,10 @@ module SwarmMemory
93
93
  "Clear old entries or use smaller content."
94
94
  end
95
95
 
96
- # Strip .md extension and flatten path for disk storage
97
- # "concepts/ruby/classes.md" → "concepts--ruby--classes"
96
+ # Strip .md extension for disk storage
97
+ # "concepts/ruby/classes.md" → "concepts/ruby/classes"
98
98
  base_path = file_path.sub(/\.md\z/, "")
99
- disk_path = flatten_path(base_path)
99
+ disk_path = base_path
100
100
 
101
101
  # 1. Write content to .md file (stored exactly as provided)
102
102
  md_file = File.join(@directory, "#{disk_path}.md")
@@ -162,9 +162,9 @@ module SwarmMemory
162
162
  return entry.content
163
163
  end
164
164
 
165
- # Strip .md extension and flatten path
165
+ # Strip .md extension
166
166
  base_path = file_path.sub(/\.md\z/, "")
167
- disk_path = flatten_path(base_path)
167
+ disk_path = base_path
168
168
  md_file = File.join(@directory, "#{disk_path}.md")
169
169
 
170
170
  raise ArgumentError, "memory://#{file_path} not found" unless File.exist?(md_file)
@@ -189,9 +189,9 @@ module SwarmMemory
189
189
  return load_virtual_entry(file_path)
190
190
  end
191
191
 
192
- # Strip .md extension and flatten path
192
+ # Strip .md extension
193
193
  base_path = file_path.sub(/\.md\z/, "")
194
- disk_path = flatten_path(base_path)
194
+ disk_path = base_path
195
195
  md_file = File.join(@directory, "#{disk_path}.md")
196
196
  yaml_file = File.join(@directory, "#{disk_path}.yml")
197
197
 
@@ -230,9 +230,9 @@ module SwarmMemory
230
230
  @semaphore.acquire do
231
231
  raise ArgumentError, "file_path is required" if file_path.nil? || file_path.to_s.strip.empty?
232
232
 
233
- # Strip .md extension and flatten path
233
+ # Strip .md extension
234
234
  base_path = file_path.sub(/\.md\z/, "")
235
- disk_path = flatten_path(base_path)
235
+ disk_path = base_path
236
236
  md_file = File.join(@directory, "#{disk_path}.md")
237
237
 
238
238
  raise ArgumentError, "memory://#{file_path} not found" unless File.exist?(md_file)
@@ -500,29 +500,6 @@ module SwarmMemory
500
500
  )
501
501
  end
502
502
 
503
- # Flatten path for disk storage
504
- # "concepts/ruby/classes" → "concepts--ruby--classes"
505
- #
506
- # @param logical_path [String] Logical path with slashes
507
- # @return [String] Flattened path with --
508
- # Identity function - paths are now stored hierarchically
509
- # Kept for backward compatibility during transition
510
- #
511
- # @param logical_path [String] Logical path
512
- # @return [String] Same path (no flattening)
513
- def flatten_path(logical_path)
514
- logical_path
515
- end
516
-
517
- # Identity function - paths are now stored hierarchically
518
- # Kept for backward compatibility during transition
519
- #
520
- # @param disk_path [String] Disk path
521
- # @return [String] Same path (no unflattening)
522
- def unflatten_path(disk_path)
523
- disk_path
524
- end
525
-
526
503
  # Check if content is a stub (redirect)
527
504
  #
528
505
  # @param content [String] File content
@@ -566,7 +543,7 @@ module SwarmMemory
566
543
  # @return [void]
567
544
  def increment_hits(file_path)
568
545
  base_path = file_path.sub(/\.md\z/, "")
569
- disk_path = flatten_path(base_path)
546
+ disk_path = base_path
570
547
  yaml_file = File.join(@directory, "#{disk_path}.yml")
571
548
  return unless File.exist?(yaml_file)
572
549
 
@@ -587,7 +564,7 @@ module SwarmMemory
587
564
  # @return [Integer] Size in bytes
588
565
  def get_entry_size(file_path)
589
566
  base_path = file_path.sub(/\.md\z/, "")
590
- disk_path = flatten_path(base_path)
567
+ disk_path = base_path
591
568
  yaml_file = File.join(@directory, "#{disk_path}.yml")
592
569
 
593
570
  if File.exist?(yaml_file)
@@ -156,7 +156,7 @@ module SwarmMemory
156
156
  # @return [String] Memory prompt contribution
157
157
  def system_prompt_contribution(agent_definition:, storage:)
158
158
  # Extract mode from memory config
159
- memory_config = agent_definition.memory
159
+ memory_config = agent_definition.plugin_config(:memory)
160
160
  mode = if memory_config.is_a?(SwarmMemory::DSL::MemoryConfig)
161
161
  memory_config.mode # MemoryConfig object from DSL
162
162
  elsif memory_config.respond_to?(:mode)
@@ -204,20 +204,100 @@ module SwarmMemory
204
204
  # @param agent_definition [Agent::Definition] Agent definition
205
205
  # @return [Boolean] True if agent has memory configuration
206
206
  def storage_enabled?(agent_definition)
207
- agent_definition.memory_enabled?
207
+ memory_config = agent_definition.plugin_config(:memory)
208
+ return false if memory_config.nil?
209
+
210
+ # MemoryConfig object (from DSL)
211
+ return memory_config.enabled? if memory_config.respond_to?(:enabled?)
212
+
213
+ # Hash (from YAML) - check for directory key
214
+ if memory_config.is_a?(Hash)
215
+ directory = memory_config[:directory] || memory_config["directory"]
216
+ return !directory.nil? && !directory.to_s.strip.empty?
217
+ end
218
+
219
+ false
208
220
  end
209
221
 
210
222
  # Contribute to agent serialization
211
223
  #
212
- # Preserves memory configuration when agents are cloned (e.g., in NodeOrchestrator).
224
+ # Preserves memory configuration when agents are cloned (e.g., in Workflow).
213
225
  # This allows memory configuration to persist across node transitions.
214
226
  #
215
227
  # @param agent_definition [Agent::Definition] Agent definition
216
228
  # @return [Hash] Memory config to include in to_h
217
229
  def serialize_config(agent_definition:)
218
- return {} unless agent_definition.memory
230
+ memory_config = agent_definition.plugin_config(:memory)
231
+ return {} unless memory_config
232
+
233
+ { memory: memory_config }
234
+ end
235
+
236
+ # Snapshot plugin-specific state for an agent
237
+ #
238
+ # Captures memory read tracking state for session persistence.
239
+ # This allows agents to remember which memory entries they've read
240
+ # across sessions.
241
+ #
242
+ # @param agent_name [Symbol] Agent identifier
243
+ # @return [Hash] Plugin-specific state
244
+ def snapshot_agent_state(agent_name)
245
+ entries_with_digests = Core::StorageReadTracker.get_read_entries(agent_name)
246
+ return {} if entries_with_digests.empty?
247
+
248
+ { read_entries: entries_with_digests }
249
+ end
250
+
251
+ # Restore plugin-specific state for an agent
252
+ #
253
+ # Restores memory read tracking state from snapshot.
254
+ # This is idempotent - calling multiple times with same state
255
+ # produces the same result.
256
+ #
257
+ # @param agent_name [Symbol] Agent identifier
258
+ # @param state [Hash] Previously snapshotted state (with symbol keys)
259
+ # @return [void]
260
+ def restore_agent_state(agent_name, state)
261
+ entries = state[:read_entries] || state["read_entries"]
262
+ return unless entries
263
+
264
+ Core::StorageReadTracker.restore_read_entries(agent_name, entries)
265
+ end
266
+
267
+ # Get digest for a memory tool result
268
+ #
269
+ # Returns the digest for a MemoryRead tool call, enabling change detection
270
+ # hooks to know if a memory entry has been modified since last read.
271
+ #
272
+ # @param agent_name [Symbol] Agent identifier
273
+ # @param tool_name [String] Name of the tool
274
+ # @param path [String] Path of the memory entry
275
+ # @return [String, nil] Digest string or nil if not a memory tool
276
+ def get_tool_result_digest(agent_name:, tool_name:, path:)
277
+ return unless tool_name == "MemoryRead"
219
278
 
220
- { memory: agent_definition.memory }
279
+ Core::StorageReadTracker.get_read_entries(agent_name)[path]
280
+ end
281
+
282
+ # Translate YAML configuration into DSL calls
283
+ #
284
+ # Called during YAML-to-DSL translation. Handles memory-specific YAML
285
+ # configuration and translates it into DSL method calls on the builder.
286
+ #
287
+ # @param builder [Agent::Builder] Builder instance (self in DSL context)
288
+ # @param agent_config [Hash] Full agent config from YAML
289
+ # @return [void]
290
+ def translate_yaml_config(builder, agent_config)
291
+ memory_config = agent_config[:memory]
292
+ return unless memory_config
293
+
294
+ builder.instance_eval do
295
+ memory do
296
+ directory(memory_config[:directory]) if memory_config[:directory]
297
+ adapter(memory_config[:adapter]) if memory_config[:adapter]
298
+ mode(memory_config[:mode]) if memory_config[:mode]
299
+ end
300
+ end
221
301
  end
222
302
 
223
303
  # Lifecycle: Agent initialized
@@ -239,7 +319,7 @@ module SwarmMemory
239
319
  return unless storage # Only proceed if memory is enabled for this agent
240
320
 
241
321
  # Extract mode from memory config
242
- memory_config = agent_definition.memory
322
+ memory_config = agent_definition.plugin_config(:memory)
243
323
  mode = if memory_config.is_a?(SwarmMemory::DSL::MemoryConfig)
244
324
  memory_config.mode # MemoryConfig object from DSL
245
325
  elsif memory_config.respond_to?(:mode)
@@ -281,7 +361,7 @@ module SwarmMemory
281
361
  agent_definition: agent_definition,
282
362
  )
283
363
 
284
- agent.with_tool(load_skill_tool)
364
+ agent.add_tool(load_skill_tool)
285
365
  end
286
366
 
287
367
  # Mark mode-specific memory tools + LoadSkill as immutable
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwarmMemory
4
- VERSION = "2.1.3"
4
+ VERSION = "2.1.4"
5
5
  end
data/lib/swarm_memory.rb CHANGED
@@ -42,7 +42,7 @@ loader.setup
42
42
  # These must be loaded after Zeitwerk but before anything uses them
43
43
  require_relative "swarm_memory/dsl/memory_config"
44
44
  require_relative "swarm_memory/dsl/builder_extension"
45
- require_relative "swarm_memory/chat_extension"
45
+ # NOTE: ChatExtension was removed in favor of SDK's built-in remove_tool method
46
46
 
47
47
  module SwarmMemory
48
48
  class << self