Class: SwarmMemory::Integration::SDKPlugin

Inherits:
SwarmSDK::Plugin
  • Object
show all
Defined in:
lib/swarm_memory/integration/sdk_plugin.rb

Overview

SwarmSDK plugin implementation for SwarmMemory

This plugin integrates SwarmMemory with SwarmSDK, providing:

  • Persistent memory storage for agents
  • Memory tools (MemoryWrite, MemoryRead, MemoryEdit, etc.)
  • LoadSkill tool for dynamic tool swapping
  • System prompt contributions for memory guidance
  • Semantic skill discovery on user messages

The plugin automatically registers itself when SwarmMemory is loaded alongside SwarmSDK.

Instance Method Summary collapse

Constructor Details

#initializeSDKPlugin

Returns a new instance of SDKPlugin.



17
18
19
20
21
22
23
24
25
26
27
28
# File 'lib/swarm_memory/integration/sdk_plugin.rb', line 17

def initialize
  super
  # Track storages for each agent: { agent_name => storage }
  # Needed for semantic skill discovery in on_user_message
  @storages = {}
  # Track memory mode for each agent: { agent_name => mode }
  # Modes: :read_write (default), :read_only, :full_access
  @modes = {}
  # Track threshold configuration for each agent: { agent_name => config }
  # Enables per-adapter threshold tuning with ENV fallback
  @threshold_configs = {}
end

Instance Method Details

#create_storage(agent_name:, config:) ⇒ Core::Storage

Create plugin storage for an agent

Parameters:

  • agent_name (Symbol)

    Agent identifier

  • config (Object)

    Memory configuration (MemoryConfig or Hash)

Returns:



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/swarm_memory/integration/sdk_plugin.rb', line 109

def create_storage(agent_name:, config:)
  # Extract adapter type and options from config
  adapter_type, adapter_options = if config.respond_to?(:adapter_type)
    # MemoryConfig object (from DSL)
    [config.adapter_type, config.adapter_options]
  elsif config.is_a?(Hash)
    # Hash (from YAML) - symbolize keys for adapter compatibility
    adapter = (config[:adapter] || config["adapter"] || :filesystem).to_sym
    options = config.reject { |k, _v| [:adapter, "adapter", :mode, "mode"].include?(k) }
    # Symbolize keys so adapter receives keyword arguments correctly
    symbolized_options = options.transform_keys { |k| k.to_s.to_sym }
    [adapter, symbolized_options]
  else
    raise SwarmSDK::ConfigurationError, "Invalid memory configuration for #{agent_name}"
  end

  # Get adapter class from registry
  begin
    adapter_class = SwarmMemory.adapter_for(adapter_type)
  rescue ArgumentError => e
    raise SwarmSDK::ConfigurationError, "#{e.message} for agent #{agent_name}"
  end

  # Extract hybrid search weights and other SDK-level config (before passing to adapter)
  # Keys are already symbolized at this point
  semantic_weight = adapter_options.delete(:semantic_weight)
  keyword_weight = adapter_options.delete(:keyword_weight)

  # Remove other SDK-level threshold configs that shouldn't go to adapter
  adapter_options.delete(:discovery_threshold)
  adapter_options.delete(:discovery_threshold_short)
  adapter_options.delete(:adaptive_word_cutoff)

  # Instantiate adapter with options (weights removed, adapter doesn't need them)
  # Note: Adapter is responsible for validating its own requirements
  begin
    adapter = adapter_class.new(**adapter_options)
  rescue ArgumentError => e
    raise SwarmSDK::ConfigurationError,
      "Failed to initialize #{adapter_type} adapter for #{agent_name}: #{e.message}"
  end

  # Create embedder for semantic search
  embedder = Embeddings::InformersEmbedder.new

  # Create storage with embedder and hybrid search weights
  Core::Storage.new(
    adapter: adapter,
    embedder: embedder,
    semantic_weight: semantic_weight,
    keyword_weight: keyword_weight,
  )
end

#create_tool(tool_name, context) ⇒ RubyLLM::Tool

Create a tool instance

Parameters:

  • tool_name (Symbol)

    Tool name

  • context (Hash)

    Creation context with :storage, :agent_name, :chat, etc.

Returns:

  • (RubyLLM::Tool)

    Tool instance



96
97
98
99
100
101
102
# File 'lib/swarm_memory/integration/sdk_plugin.rb', line 96

def create_tool(tool_name, context)
  storage = context[:storage]
  agent_name = context[:agent_name]

  # Delegate to SwarmMemory's tool factory
  SwarmMemory.create_tool(tool_name, storage: storage, agent_name: agent_name)
end

#get_tool_result_digest(agent_name:, tool_name:, path:) ⇒ String?

Get digest for a memory tool result

Returns the digest for a MemoryRead tool call, enabling change detection hooks to know if a memory entry has been modified since last read.

Parameters:

  • agent_name (Symbol)

    Agent identifier

  • tool_name (String)

    Name of the tool

  • path (String)

    Path of the memory entry

Returns:

  • (String, nil)

    Digest string or nil if not a memory tool



290
291
292
293
294
# File 'lib/swarm_memory/integration/sdk_plugin.rb', line 290

def get_tool_result_digest(agent_name:, tool_name:, path:)
  return unless tool_name == "MemoryRead"

  Core::StorageReadTracker.get_read_entries(agent_name)[path]
end

#memory_configured?(agent_definition) ⇒ Boolean

Check if memory is configured for this agent

Delegates adapter-specific validation to the adapter itself. Filesystem adapter requires 'directory', custom adapters may use other keys.

Parameters:

  • agent_definition (Agent::Definition)

    Agent definition

Returns:

  • (Boolean)

    True if agent has valid memory configuration



211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# File 'lib/swarm_memory/integration/sdk_plugin.rb', line 211

def memory_configured?(agent_definition)
  memory_config = agent_definition.plugin_config(:memory)
  return false if memory_config.nil?

  # MemoryConfig object (from DSL) - delegates to its enabled? method
  return memory_config.enabled? if memory_config.respond_to?(:enabled?)

  # Hash (from YAML)
  return false unless memory_config.is_a?(Hash)
  return false if memory_config.empty?

  adapter = (memory_config[:adapter] || memory_config["adapter"] || :filesystem).to_sym

  case adapter
  when :filesystem
    # Filesystem adapter requires directory
    directory = memory_config[:directory] || memory_config["directory"]
    !directory.nil? && !directory.to_s.strip.empty?
  else
    # Custom adapters: presence of config is sufficient
    # Adapter will validate its own requirements during initialization
    true
  end
end

#nameSymbol

Plugin identifier

Returns:

  • (Symbol)

    Plugin name



33
34
35
# File 'lib/swarm_memory/integration/sdk_plugin.rb', line 33

def name
  :memory
end

#on_agent_initialized(agent_name:, agent:, context:) ⇒ Object

Lifecycle: Agent initialized

Filters tools by mode (removing non-mode tools), registers LoadSkill, and marks memory tools as immutable.

LoadSkill needs special handling because it requires chat, tool_configurator, and agent_definition to perform dynamic tool swapping.

Parameters:

  • agent_name (Symbol)

    Agent identifier

  • agent (Agent::Chat)

    Chat instance

  • context (Hash)

    Initialization context



344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
# File 'lib/swarm_memory/integration/sdk_plugin.rb', line 344

def on_agent_initialized(agent_name:, agent:, context:)
  storage = context[:storage]
  agent_definition = context[:agent_definition]
  tool_configurator = context[:tool_configurator]

  return unless storage # Only proceed if memory is enabled for this agent

  # Extract mode from memory config
  memory_config = agent_definition.plugin_config(:memory)
  mode = if memory_config.is_a?(SwarmMemory::DSL::MemoryConfig)
    memory_config.mode # MemoryConfig object from DSL
  elsif memory_config.respond_to?(:mode)
    memory_config.mode # Other object with mode method
  elsif memory_config.is_a?(Hash)
    (memory_config[:mode] || memory_config["mode"] || :interactive).to_sym
  else
    :interactive # Default
  end

  # V7.0: Extract base name for storage tracking (delegation instances share storage)
  base_name = agent_name.to_s.split("@").first.to_sym

  # Store storage and mode using BASE NAME
  @storages[base_name] = storage # ← Changed from agent_name to base_name
  @modes[base_name] = mode # ← Changed from agent_name to base_name
  @threshold_configs[base_name] = extract_threshold_config(memory_config)

  # NOTE: Memory tools are already registered by ToolConfigurator.register_plugin_tools
  # We need to unregister tools not allowed in this mode (Plan 025)

  all_memory_tools = tools
  allowed_tools = tools_for_mode(mode)
  tools_to_remove = all_memory_tools - allowed_tools

  # Unregister tools not allowed in this mode
  tools_to_remove.each do |tool_name|
    agent.tool_registry.unregister(tool_name.to_s)
  end

  # Create and register LoadSkill tool (NOT for read_only mode)
  unless mode == :read_only
    load_skill_tool = SwarmMemory.create_tool(
      :LoadSkill,
      storage: storage,
      agent_name: agent_name,
      chat: agent,
      tool_configurator: tool_configurator,
      agent_definition: agent_definition,
    )

    # Register in tool registry (Plan 025)
    agent.tool_registry.register(
      load_skill_tool,
      source: :plugin,
      metadata: { plugin_name: :memory, mode: mode },
    )
  end

  # NOTE: No need to mark tools immutable - they declare removable? themselves (Plan 025)
end

#on_user_message(agent_name:, prompt:, is_first_message:) ⇒ Array<String>

Lifecycle: User message

Performs TWO semantic searches:

  1. Skills - For loadable procedures with LoadSkill
  2. Memories - For concepts/facts/experiences that provide context

Returns system reminders for both if high-confidence matches found.

Parameters:

  • agent_name (Symbol)

    Agent identifier

  • prompt (String)

    User's message

  • is_first_message (Boolean)

    True if first message

Returns:

  • (Array<String>)

    System reminders (0-2 reminders)



417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
# File 'lib/swarm_memory/integration/sdk_plugin.rb', line 417

def on_user_message(agent_name:, prompt:, is_first_message:)
  # V7.0: Extract base name for storage lookup (delegation instances share storage)
  base_name = agent_name.to_s.split("@").first.to_sym
  storage = @storages[base_name] # ← Changed from agent_name to base_name
  config = @threshold_configs[base_name] || {}

  return [] unless storage&.semantic_index
  return [] if prompt.nil? || prompt.empty?

  # Adaptive threshold based on query length
  # Short queries use lower threshold as they have less semantic richness
  # Fallback chain: config → ENV → default
  word_count = prompt.split.size
  word_cutoff = config[:adaptive_word_cutoff] ||
    ENV["SWARM_MEMORY_ADAPTIVE_WORD_CUTOFF"]&.to_i ||
    10

  threshold = if word_count < word_cutoff
    config[:discovery_threshold_short] ||
      ENV["SWARM_MEMORY_DISCOVERY_THRESHOLD_SHORT"]&.to_f ||
      0.25
  else
    config[:discovery_threshold] ||
      ENV["SWARM_MEMORY_DISCOVERY_THRESHOLD"]&.to_f ||
      0.35
  end
  reminders = []

  # Run both searches in parallel with Async
  Async do |task|
    # Search 1: Skills (type = "skill")
    skills_task = task.async do
      storage.semantic_index.search(
        query: prompt,
        top_k: 3,
        threshold: threshold,
        filter: { "type" => "skill" },
      )
    end

    # Search 2: All results (for memories + logging)
    all_results_task = task.async do
      storage.semantic_index.search(
        query: prompt,
        top_k: 10,
        threshold: 0.0, # Get all for logging
        filter: nil,
      )
    end

    # Wait for both searches to complete
    skills = skills_task.wait
    all_results = all_results_task.wait

    # Filter to concepts, facts, experiences (not skills)
    memories = all_results
      .select { |r| ["concept", "fact", "experience"].include?(r.dig(:metadata, "type")) }
      .select { |r| r[:similarity] >= threshold }
      .take(3)

    # Emit log events (include word count for adaptive threshold analysis)
    search_context = { threshold: threshold, word_count: word_count, word_cutoff: word_cutoff }
    emit_skill_search_log(agent_name, prompt, skills, all_results, search_context)
    emit_memory_search_log(agent_name, prompt, memories, all_results, search_context)

    # Build skill reminder if found
    if skills.any?
      reminders << build_skill_discovery_reminder(skills)
    end

    # Build memory reminder if found
    if memories.any?
      reminders << build_memory_discovery_reminder(memories)
    end
  end.wait

  reminders
end

#parse_config(raw_config) ⇒ Object

Parse memory configuration

Parameters:

  • raw_config (Object)

    Raw config (MemoryConfig or Hash)

Returns:

  • (Object)

    Parsed configuration



167
168
169
170
# File 'lib/swarm_memory/integration/sdk_plugin.rb', line 167

def parse_config(raw_config)
  # Already parsed by Agent::Definition, just return as-is
  raw_config
end

#restore_agent_state(agent_name, state) ⇒ void

This method returns an undefined value.

Restore plugin-specific state for an agent

Restores memory read tracking state from snapshot. This is idempotent - calling multiple times with same state produces the same result.

Parameters:

  • agent_name (Symbol)

    Agent identifier

  • state (Hash)

    Previously snapshotted state (with symbol keys)



274
275
276
277
278
279
# File 'lib/swarm_memory/integration/sdk_plugin.rb', line 274

def restore_agent_state(agent_name, state)
  entries = state[:read_entries] || state["read_entries"]
  return unless entries

  Core::StorageReadTracker.restore_read_entries(agent_name, entries)
end

#serialize_config(agent_definition:) ⇒ Hash

Contribute to agent serialization

Preserves memory configuration when agents are cloned (e.g., in Workflow). This allows memory configuration to persist across node transitions.

Parameters:

  • agent_definition (Agent::Definition)

    Agent definition

Returns:

  • (Hash)

    Memory config to include in to_h



243
244
245
246
247
248
# File 'lib/swarm_memory/integration/sdk_plugin.rb', line 243

def serialize_config(agent_definition:)
  memory_config = agent_definition.plugin_config(:memory)
  return {} unless memory_config

  { memory: memory_config }
end

#snapshot_agent_state(agent_name) ⇒ Hash

Snapshot plugin-specific state for an agent

Captures memory read tracking state for session persistence. This allows agents to remember which memory entries they've read across sessions.

Parameters:

  • agent_name (Symbol)

    Agent identifier

Returns:

  • (Hash)

    Plugin-specific state



258
259
260
261
262
263
# File 'lib/swarm_memory/integration/sdk_plugin.rb', line 258

def snapshot_agent_state(agent_name)
  entries_with_digests = Core::StorageReadTracker.get_read_entries(agent_name)
  return {} if entries_with_digests.empty?

  { read_entries: entries_with_digests }
end

#system_prompt_contribution(agent_definition:, storage:) ⇒ String

Contribute to agent system prompt

Parameters:

  • agent_definition (Agent::Definition)

    Agent definition

  • storage (Core::Storage, nil)

    Storage instance (may be nil during prompt building)

Returns:

  • (String)

    Memory prompt contribution



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
# File 'lib/swarm_memory/integration/sdk_plugin.rb', line 177

def system_prompt_contribution(agent_definition:, storage:)
  # Extract mode from memory config
  memory_config = agent_definition.plugin_config(:memory)
  mode = if memory_config.is_a?(SwarmMemory::DSL::MemoryConfig)
    memory_config.mode # MemoryConfig object from DSL
  elsif memory_config.respond_to?(:mode)
    memory_config.mode # Other object with mode method
  elsif memory_config.is_a?(Hash)
    (memory_config[:mode] || memory_config["mode"] || :read_write).to_sym
  else
    :read_write # Default mode
  end

  # Select prompt template based on mode
  prompt_filename = case mode
  when :read_only then "memory_read_only.md.erb"
  when :full_access then "memory_full_access.md.erb"
  else "memory_read_write.md.erb" # Default
  end

  memory_prompt_path = File.expand_path("../prompts/#{prompt_filename}", __dir__)
  template_content = File.read(memory_prompt_path)

  # Render with agent_definition binding
  ERB.new(template_content).result(agent_definition.instance_eval { binding })
end

#toolsArray<Symbol>

Tools provided by this plugin

Returns all memory tools for PluginRegistry mapping. Tools are auto-registered by ToolConfigurator, then filtered by mode in on_agent_initialized using remove_tool.

Note: LoadSkill is NOT included here because it requires special handling. It's registered separately in on_agent_initialized lifecycle hook because it needs chat, tool_configurator, and agent_definition parameters.

Returns:

  • (Array<Symbol>)

    All memory tool names



48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/swarm_memory/integration/sdk_plugin.rb', line 48

def tools
  [
    :MemoryRead,
    :MemoryGlob,
    :MemoryGrep,
    :MemorySearch,
    :MemoryWrite,
    :MemoryEdit,
    :MemoryDelete,
    :MemoryDefrag,
  ]
end

#tools_for_mode(mode) ⇒ Array<Symbol>

Get tools for a specific mode

Parameters:

  • mode (Symbol)

    Memory mode

Returns:

  • (Array<Symbol>)

    Tool names for this mode



65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
# File 'lib/swarm_memory/integration/sdk_plugin.rb', line 65

def tools_for_mode(mode)
  case mode
  when :read_only
    # Read-only tools for Q&A agents
    [:MemoryRead, :MemoryGlob, :MemoryGrep, :MemorySearch]
  when :read_write
    # Read + Write + Edit for learning agents (need edit for corrections)
    [:MemoryRead, :MemoryGlob, :MemoryGrep, :MemorySearch, :MemoryWrite, :MemoryEdit]
  when :full_access
    # All tools for knowledge extraction and management
    [
      :MemoryRead,
      :MemoryGlob,
      :MemoryGrep,
      :MemorySearch,
      :MemoryWrite,
      :MemoryEdit,
      :MemoryDelete,
      :MemoryDefrag,
    ]
  else
    # Default to read_write
    [:MemoryRead, :MemoryGlob, :MemoryGrep, :MemorySearch, :MemoryWrite, :MemoryEdit]
  end
end

#translate_yaml_config(builder, agent_config) ⇒ void

This method returns an undefined value.

Translate YAML configuration into DSL calls

Called during YAML-to-DSL translation. Handles memory-specific YAML configuration and translates it into DSL method calls on the builder.

Parameters:

  • builder (Agent::Builder)

    Builder instance (self in DSL context)

  • agent_config (Hash)

    Full agent config from YAML



304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
# File 'lib/swarm_memory/integration/sdk_plugin.rb', line 304

def translate_yaml_config(builder, agent_config)
  memory_config = agent_config[:memory]
  return unless memory_config

  builder.instance_eval do
    memory do
      # Standard options
      directory(memory_config[:directory]) if memory_config[:directory]
      adapter(memory_config[:adapter]) if memory_config[:adapter]
      mode(memory_config[:mode]) if memory_config[:mode]

      # Pass through all custom adapter options
      # Handle both symbol and string keys (YAML may have either)
      standard_keys = [
        :directory,
        :adapter,
        :mode,
        "directory",
        "adapter",
        "mode",
      ]
      custom_keys = memory_config.keys - standard_keys
      custom_keys.each do |key|
        option(key.to_sym, memory_config[key]) # Normalize to symbol
      end
    end
  end
end