Class: HTM::RobotGroup

Inherits:
Object
  • Object
show all
Defined in:
lib/htm/robot_group.rb

Overview

Coordinates multiple robots with shared working memory and automatic failover.

RobotGroup provides application-level coordination for multiple HTM robots, enabling them to share a common working memory context. Key capabilities include:

  • **Shared Working Memory**: All group members have access to the same context

  • **Active/Passive Roles**: Active robots participate in conversations; passive robots maintain synchronized context for instant failover

  • **Real-time Sync**: PostgreSQL LISTEN/NOTIFY enables immediate synchronization

  • Failover: When an active robot fails, a passive robot takes over instantly

  • **Dynamic Scaling**: Add or remove robots at runtime

Examples:

High-availability customer support setup

group = HTM::RobotGroup.new(
  name: 'customer-support',
  active: ['primary-agent'],
  passive: ['standby-agent'],
  max_tokens: 8000
)

# Add shared context
group.remember('Customer prefers email communication.')
group.remember('Open ticket #789 regarding billing issue.')

# Query shared memory
results = group.recall('billing', limit: 5)

# Simulate failover
group.failover!  # Promotes standby to active

# Cleanup
group.shutdown

See Also:

Instance Attribute Summary collapse

Membership Management collapse

Shared Working Memory Operations collapse

Synchronization collapse

Failover collapse

Status & Health collapse

Instance Method Summary collapse

Constructor Details

#initialize(name:, active: [], passive: [], max_tokens: 4000, db_config: nil) ⇒ RobotGroup

Creates a new robot group with optional initial members.

Initializes the group, sets up the PostgreSQL pub/sub channel for real-time synchronization, and registers initial active and passive robots.

Examples:

Create a group with one active and one passive robot

group = HTM::RobotGroup.new(
  name: 'support-team',
  active: ['agent-1'],
  passive: ['agent-2'],
  max_tokens: 4000
)

Create an empty group and add members later

group = HTM::RobotGroup.new(name: 'dynamic-team')
group.add_active('agent-1')
group.add_passive('agent-2')

Parameters:

  • name (String)

    Unique name for this robot group

  • active (Array<String>) (defaults to: [])

    Names of robots to add as active members

  • passive (Array<String>) (defaults to: [])

    Names of robots to add as passive (standby) members

  • max_tokens (Integer) (defaults to: 4000)

    Maximum token budget for shared working memory

  • db_config (Hash, nil) (defaults to: nil)

    PostgreSQL connection config (defaults to HTM::Database.default_config)



77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
# File 'lib/htm/robot_group.rb', line 77

def initialize(name:, active: [], passive: [], max_tokens: 4000, db_config: nil)
  @name           = name
  @max_tokens     = max_tokens
  @active_robots  = {} # name => HTM instance
  @passive_robots = {} # name => HTM instance
  @sync_stats     = { nodes_synced: 0, evictions_synced: 0 }
  @mutex          = Mutex.new

  # Setup pub/sub channel for real-time sync
  @db_config  = db_config || HTM::Database.default_config
  @channel    = HTM::WorkingMemoryChannel.new(name, @db_config)

  # Subscribe to working memory changes
  setup_sync_listener

  # Start listening for notifications
  @channel.start_listening

  # Initialize robots
  active.each  { |robot_name| add_active(robot_name) }
  passive.each { |robot_name| add_passive(robot_name) }
end

Instance Attribute Details

#channelHTM::WorkingMemoryChannel (readonly)

The pub/sub channel used for real-time synchronization



51
52
53
# File 'lib/htm/robot_group.rb', line 51

def channel
  @channel
end

#max_tokensInteger (readonly)

Maximum token budget for working memory

Returns:

  • (Integer)


47
48
49
# File 'lib/htm/robot_group.rb', line 47

def max_tokens
  @max_tokens
end

#nameString (readonly)

Name of the robot group

Returns:

  • (String)


43
44
45
# File 'lib/htm/robot_group.rb', line 43

def name
  @name
end

Instance Method Details

#active?(robot_name) ⇒ Boolean

Checks if a robot is an active member of this group.

Examples:

group.active?('primary-agent')  # => true

Parameters:

  • robot_name (String)

    Name of the robot to check

Returns:

  • (Boolean)

    true if the robot is an active member



249
250
251
# File 'lib/htm/robot_group.rb', line 249

def active?(robot_name)
  @active_robots.key?(robot_name)
end

#active_robot_namesArray<String>

Returns names of all active robots.

Examples:

group.active_robot_names  # => ['primary-agent', 'secondary-agent']

Returns:

  • (Array<String>)

    Array of active robot names



283
284
285
# File 'lib/htm/robot_group.rb', line 283

def active_robot_names
  @active_robots.keys
end

#add_active(robot_name) ⇒ Integer

Adds a robot as an active member of the group.

Active robots can add memories and respond to queries. The new robot is automatically synchronized with existing shared working memory.

Examples:

robot_id = group.add_active('new-agent')
puts "Added robot with ID: #{robot_id}"

Parameters:

  • robot_name (String)

    Unique name for the robot

Returns:

  • (Integer)

    The robot’s database ID

Raises:

  • (ArgumentError)

    if robot_name is already a member



129
130
131
132
133
134
135
136
137
138
139
# File 'lib/htm/robot_group.rb', line 129

def add_active(robot_name)
  raise ArgumentError, "#{robot_name} is already a member" if member?(robot_name)

  htm = HTM.new(robot_name: robot_name, working_memory_size: @max_tokens)
  @active_robots[robot_name] = htm

  # Sync existing shared working memory to new member
  sync_robot(robot_name) if member_ids.length > 1

  htm.robot_id
end

#add_passive(robot_name) ⇒ Integer

Adds a robot as a passive (standby) member of the group.

Passive robots maintain synchronized working memory but don’t actively participate in conversations. They serve as warm standbys for failover.

Examples:

robot_id = group.add_passive('standby-agent')

Parameters:

  • robot_name (String)

    Unique name for the robot

Returns:

  • (Integer)

    The robot’s database ID

Raises:

  • (ArgumentError)

    if robot_name is already a member



153
154
155
156
157
158
159
160
161
162
163
# File 'lib/htm/robot_group.rb', line 153

def add_passive(robot_name)
  raise ArgumentError, "#{robot_name} is already a member" if member?(robot_name)

  htm = HTM.new(robot_name: robot_name, working_memory_size: @max_tokens)
  @passive_robots[robot_name] = htm

  # Sync existing shared working memory to new member
  sync_robot(robot_name) if member_ids.length > 1

  htm.robot_id
end

#clear_working_memoryInteger

Clears shared working memory for all group members.

Updates database flags and notifies all members to clear their in-memory caches.

Examples:

cleared_count = group.clear_working_memory
puts "Cleared #{cleared_count} working memory entries"

Returns:

  • (Integer)

    Number of robot_node records updated



397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
# File 'lib/htm/robot_group.rb', line 397

def clear_working_memory
  count = HTM::Models::RobotNode
          .where(robot_id: member_ids, working_memory: true)
          .update(working_memory: false)

  # Clear in-memory working memory for primary robot
  primary = @active_robots.values.first || @passive_robots.values.first
  return 0 unless primary

  primary.clear_working_memory

  # Notify all listeners (will clear other in-memory caches via callback)
  @channel.notify(:cleared, node_id: nil, robot_id: primary.robot_id)

  count
end

#demote(robot_name) ⇒ void

This method returns an undefined value.

Demotes an active robot to passive status.

The robot retains its working memory but stops handling queries. Cannot demote the last active robot.

Examples:

group.demote('primary-agent')
group.passive?('primary-agent')  # => true

Parameters:

  • robot_name (String)

    Name of the active robot to demote

Raises:

  • (ArgumentError)

    if robot_name is not an active member

  • (ArgumentError)

    if this is the last active robot



220
221
222
223
224
225
226
# File 'lib/htm/robot_group.rb', line 220

def demote(robot_name)
  raise ArgumentError, "#{robot_name} is not an active member" unless active?(robot_name)
  raise ArgumentError, 'Cannot demote last active robot' if @active_robots.length == 1

  htm = @active_robots.delete(robot_name)
  @passive_robots[robot_name] = htm
end

#failover!String

Performs automatic failover to the first passive robot.

Promotes the first passive robot to active status. The promoted robot already has synchronized working memory and can immediately handle requests.

Examples:

promoted = group.failover!
puts "#{promoted} is now active"

Returns:

  • (String)

    Name of the promoted robot

Raises:

  • (RuntimeError)

    if no passive robots are available



589
590
591
592
593
594
595
596
597
598
599
600
# File 'lib/htm/robot_group.rb', line 589

def failover!
  raise 'No passive robots available for failover' if @passive_robots.empty?

  # Get first passive robot
  standby_name = @passive_robots.keys.first

  # Promote it
  promote(standby_name)

  puts "  ⚡ Failover: #{standby_name} promoted to active"
  standby_name
end

#in_sync?Boolean

Checks if all members have identical working memory.

Compares the set of working memory node IDs across all members.

Examples:

if group.in_sync?
  puts "All robots synchronized"
else
  group.sync_all
end

Returns:

  • (Boolean)

    true if all members have the same working memory nodes



501
502
503
504
505
506
507
508
509
510
511
512
513
514
# File 'lib/htm/robot_group.rb', line 501

def in_sync?
  return true if member_ids.length <= 1

  # Get working memory node_ids for each robot
  working_memories = member_ids.map do |robot_id|
    HTM::Models::RobotNode
      .where(robot_id: robot_id, working_memory: true)
      .select_map(:node_id)
      .sort
  end

  # All should be identical
  working_memories.uniq.length == 1
end

#member?(robot_name) ⇒ Boolean

Checks if a robot is a member of this group.

Examples:

group.member?('agent-1')  # => true
group.member?('unknown')  # => false

Parameters:

  • robot_name (String)

    Name of the robot to check

Returns:

  • (Boolean)

    true if the robot is an active or passive member



237
238
239
# File 'lib/htm/robot_group.rb', line 237

def member?(robot_name)
  @active_robots.key?(robot_name) || @passive_robots.key?(robot_name)
end

#member_idsArray<Integer>

Returns database IDs of all group members.

Examples:

group.member_ids  # => [1, 2, 3]

Returns:

  • (Array<Integer>)

    Array of robot IDs (both active and passive)



272
273
274
# File 'lib/htm/robot_group.rb', line 272

def member_ids
  all_robots.values.map(&:robot_id)
end

#passive?(robot_name) ⇒ Boolean

Checks if a robot is a passive member of this group.

Examples:

group.passive?('standby-agent')  # => true

Parameters:

  • robot_name (String)

    Name of the robot to check

Returns:

  • (Boolean)

    true if the robot is a passive member



261
262
263
# File 'lib/htm/robot_group.rb', line 261

def passive?(robot_name)
  @passive_robots.key?(robot_name)
end

#passive_robot_namesArray<String>

Returns names of all passive robots.

Examples:

group.passive_robot_names  # => ['standby-agent']

Returns:

  • (Array<String>)

    Array of passive robot names



294
295
296
# File 'lib/htm/robot_group.rb', line 294

def passive_robot_names
  @passive_robots.keys
end

#promote(robot_name) ⇒ void

This method returns an undefined value.

Promotes a passive robot to active status.

The robot retains its synchronized working memory and becomes eligible to handle queries and add memories.

Examples:

group.promote('standby-agent')
group.active?('standby-agent')  # => true

Parameters:

  • robot_name (String)

    Name of the passive robot to promote

Raises:

  • (ArgumentError)

    if robot_name is not a passive member



199
200
201
202
203
204
# File 'lib/htm/robot_group.rb', line 199

def promote(robot_name)
  raise ArgumentError, "#{robot_name} is not a passive member" unless passive?(robot_name)

  htm = @passive_robots.delete(robot_name)
  @active_robots[robot_name] = htm
end

#recall(query) ⇒ Array

Recalls memories from shared working memory.

Uses the first active robot to perform the query against the shared working memory context.

Examples:

results = group.recall('billing issue', limit: 5, strategy: :fulltext)

Parameters:

  • query (String)

    The search query

  • options (Hash)

    Additional options passed to HTM#recall

Returns:

  • (Array)

    Array of matching memories

Raises:

  • (RuntimeError)

    if no active robots exist in the group



359
360
361
362
363
364
# File 'lib/htm/robot_group.rb', line 359

def recall(query, **)
  raise 'No active robots in group' if @active_robots.empty?

  primary = @active_robots.values.first
  primary.recall(query, **)
end

#remember(content, originator: nil) ⇒ Integer

Adds content to shared working memory for all group members.

The memory is created by the specified originator (or first active robot) and automatically synchronized to all other members via database and real-time notifications.

Examples:

Add memory with default originator

node_id = group.remember('Customer prefers morning appointments.')

Add memory with specific originator

node_id = group.remember(
  'Escalated to billing department.',
  originator: 'agent-2'
)

Parameters:

  • content (String)

    The content to remember

  • originator (String, nil) (defaults to: nil)

    Name of the robot creating the memory (optional)

  • options (Hash)

    Additional options passed to HTM#remember

Returns:

  • (Integer)

    The node ID of the created memory

Raises:

  • (RuntimeError)

    if no active robots exist in the group



323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
# File 'lib/htm/robot_group.rb', line 323

def remember(content, originator: nil, **)
  raise 'No active robots in group' if @active_robots.empty?

  # Use first active robot (or specified originator) to create the memory
  primary = if originator && all_robots[originator]
              all_robots[originator]
            else
              @active_robots.values.first
            end

  node_id = primary.remember(content, **)

  # Sync to database (robot_nodes table) for all other members
  sync_node_to_members(node_id, exclude: primary.robot_id)

  # Notify all listeners via PostgreSQL NOTIFY (triggers in-memory sync)
  @channel.notify(:added, node_id: node_id, robot_id: primary.robot_id)

  node_id
end

#remove(robot_name) ⇒ void

This method returns an undefined value.

Removes a robot from the group.

Clears the robot’s working memory flags in the database. The robot can be either active or passive.

Examples:

group.remove('departing-agent')

Parameters:

  • robot_name (String)

    Name of the robot to remove



176
177
178
179
180
181
182
183
184
# File 'lib/htm/robot_group.rb', line 176

def remove(robot_name)
  htm = @active_robots.delete(robot_name) || @passive_robots.delete(robot_name)
  return unless htm

  # Clear working memory flags for this robot
  HTM::Models::RobotNode
    .where(robot_id: htm.robot_id, working_memory: true)
    .update(working_memory: false)
end

#shutdownvoid

This method returns an undefined value.

Shuts down the group by stopping the listener thread.

Should be called when the group is no longer needed to release resources and close the PostgreSQL listener connection.

Examples:

group.shutdown


110
111
112
# File 'lib/htm/robot_group.rb', line 110

def shutdown
  @channel.stop_listening
end

#statusHash

Returns comprehensive status information about the group.

Examples:

status = group.status
puts "Group: #{status[:name]}"
puts "Active: #{status[:active].join(', ')}"
puts "Utilization: #{(status[:token_utilization] * 100).round(1)}%"

Parameters:

  • return (Hash)

    a customizable set of options

Returns:

  • (Hash)

    Status hash with the following keys:



625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
# File 'lib/htm/robot_group.rb', line 625

def status
  wm_contents = working_memory_contents
  token_count = wm_contents.sum { |n| n.token_count || 0 }

  {
    name: @name,
    active: active_robot_names,
    passive: passive_robot_names,
    total_members: member_ids.length,
    working_memory_nodes: wm_contents.count,
    working_memory_tokens: token_count,
    max_tokens: @max_tokens,
    token_utilization: @max_tokens.positive? ? (token_count.to_f / @max_tokens).round(2) : 0,
    in_sync: in_sync?
  }
end

#sync_allHash

Synchronizes all members to a consistent state.

Ensures every member has access to all shared working memory nodes.

Examples:

result = group.sync_all
puts "Synced #{result[:synced_nodes]} nodes to #{result[:members_updated]} members"

Returns:

  • (Hash)

    Sync results with :synced_nodes and :members_updated counts



473
474
475
476
477
478
479
480
481
482
483
484
485
486
# File 'lib/htm/robot_group.rb', line 473

def sync_all
  members_updated = 0
  total_synced    = 0

  all_robots.each_key do |robot_name|
    synced = sync_robot(robot_name)
    if synced.positive?
      members_updated += 1
      total_synced    += synced
    end
  end

  { synced_nodes: total_synced, members_updated: members_updated }
end

#sync_robot(robot_name) ⇒ Integer

Synchronizes a specific robot to match the group’s shared working memory.

Copies working memory flags from other members to the specified robot, ensuring it has access to all shared context.

Examples:

synced = group.sync_robot('new-agent')
puts "Synchronized #{synced} nodes"

Parameters:

  • robot_name (String)

    Name of the robot to synchronize

Returns:

  • (Integer)

    Number of nodes synchronized

Raises:

  • (ArgumentError)

    if robot_name is not a member



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
# File 'lib/htm/robot_group.rb', line 431

def sync_robot(robot_name)
  htm = all_robots[robot_name]
  raise ArgumentError, "#{robot_name} is not a member" unless htm

  # Get all node_ids currently in any member's working memory
  shared_node_ids = HTM::Models::RobotNode
                    .where(robot_id: member_ids, working_memory: true)
                    .exclude(robot_id: htm.robot_id)
                    .distinct
                    .select_map(:node_id)

  synced = 0
  shared_node_ids.each do |node_id|
    # Create or update robot_node with working_memory=true
    robot_node = HTM::Models::RobotNode.first(
      robot_id: htm.robot_id,
      node_id: node_id
    )
    robot_node ||= HTM::Models::RobotNode.new(
      robot_id: htm.robot_id,
      node_id: node_id
    )
    next if robot_node.working_memory

    robot_node.working_memory = true
    robot_node.save
    synced += 1
  end

  synced
end

#sync_statsHash

Returns statistics about real-time synchronization.

Examples:

stats = group.sync_stats
puts "Nodes synced: #{stats[:nodes_synced]}"
puts "Evictions synced: #{stats[:evictions_synced]}"

Returns:

  • (Hash)

    Stats hash with :nodes_synced and :evictions_synced counts



651
652
653
# File 'lib/htm/robot_group.rb', line 651

def sync_stats
  @mutex.synchronize { @sync_stats.dup }
end

#transfer_working_memory(from_robot, to_robot, clear_source: true) ⇒ Integer

Transfers working memory from one robot to another.

Copies all working memory node references from the source robot to the target robot, optionally clearing the source.

Examples:

Transfer with source clearing

transferred = group.transfer_working_memory('failing-agent', 'backup-agent')

Transfer without clearing source

transferred = group.transfer_working_memory(
  'agent-1', 'agent-2',
  clear_source: false
)

Parameters:

  • from_robot (String)

    Name of the source robot

  • to_robot (String)

    Name of the destination robot

  • clear_source (Boolean) (defaults to: true)

    Whether to clear source’s working memory after transfer

Returns:

  • (Integer)

    Number of nodes transferred

Raises:

  • (ArgumentError)

    if either robot is not a member



540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
# File 'lib/htm/robot_group.rb', line 540

def transfer_working_memory(from_robot, to_robot, clear_source: true)
  from_htm = all_robots[from_robot]
  to_htm = all_robots[to_robot]

  raise ArgumentError, "#{from_robot} is not a member" unless from_htm
  raise ArgumentError, "#{to_robot} is not a member" unless to_htm

  # Get source's working memory nodes
  source_node_ids = HTM::Models::RobotNode
                    .where(robot_id: from_htm.robot_id, working_memory: true)
                    .select_map(:node_id)

  transferred = 0
  source_node_ids.each do |node_id|
    robot_node = HTM::Models::RobotNode.first(
      robot_id: to_htm.robot_id,
      node_id: node_id
    )
    robot_node ||= HTM::Models::RobotNode.new(
      robot_id: to_htm.robot_id,
      node_id: node_id
    )
    robot_node.working_memory = true
    robot_node.save
    transferred += 1
  end

  # Clear source's working memory if requested
  if clear_source
    HTM::Models::RobotNode
      .where(robot_id: from_htm.robot_id, working_memory: true)
      .update(working_memory: false)
  end

  transferred
end

#working_memory_contentsSequel::Dataset

Returns all nodes currently in shared working memory.

Queries the database for the union of all members’ working memory, returning nodes sorted by creation date (newest first).

Examples:

nodes = group.working_memory_contents
nodes.each { |n| puts n.content }

Returns:

  • (Sequel::Dataset)

    Collection of nodes



377
378
379
380
381
382
383
384
# File 'lib/htm/robot_group.rb', line 377

def working_memory_contents
  node_ids = HTM::Models::RobotNode
             .where(robot_id: member_ids, working_memory: true)
             .distinct
             .select_map(:node_id)

  HTM::Models::Node.where(id: node_ids).order(Sequel.desc(:created_at))
end