lex-mesh 0.1.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 6b30e6f5f2a0518bde780c8b661c910fe26ec503ddb285ce60883120f8826825
4
+ data.tar.gz: 456c2c731747543cebb0515abfc44b497382f6c3ca10eb0ad52b21f5816367f8
5
+ SHA512:
6
+ metadata.gz: 3b6044cd55148c45c91ce715476a0566a8f2aa6a4aafba4607d0049b80eeffe3578e31ceb6e53a5828b9e857335b0a72a6f4eb3af170fd2ca60c4ce4047da534
7
+ data.tar.gz: c8002ef1a93733b0e8f7848bf4db6903c90146cd67882d015c6b2d229b3f03dcff3d2abc2fcdeefb0c7b1ab043d1b5a6c172a129475f1044e1063dad6273d63e
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ gem 'rspec', '~> 3.13'
8
+ gem 'rubocop', '~> 1.75', require: false
9
+
10
+ gem 'legion-gaia', path: '../../legion-gaia'
data/lex-mesh.gemspec ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/legion/extensions/mesh/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'lex-mesh'
7
+ spec.version = Legion::Extensions::Mesh::VERSION
8
+ spec.authors = ['Esity']
9
+ spec.email = ['matthewdiverson@gmail.com']
10
+
11
+ spec.summary = 'LEX Mesh'
12
+ spec.description = 'Agent-to-agent mesh communication protocol for brain-modeled agentic AI'
13
+ spec.homepage = 'https://github.com/LegionIO/lex-mesh'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = '>= 3.4'
16
+
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = 'https://github.com/LegionIO/lex-mesh'
19
+ spec.metadata['documentation_uri'] = 'https://github.com/LegionIO/lex-mesh'
20
+ spec.metadata['changelog_uri'] = 'https://github.com/LegionIO/lex-mesh'
21
+ spec.metadata['bug_tracker_uri'] = 'https://github.com/LegionIO/lex-mesh/issues'
22
+ spec.metadata['rubygems_mfa_required'] = 'true'
23
+
24
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
25
+ Dir.glob('{lib,spec}/**/*') + %w[lex-mesh.gemspec Gemfile]
26
+ end
27
+ spec.require_paths = ['lib']
28
+ spec.add_development_dependency 'legion-gaia'
29
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/actors/every'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Mesh
8
+ module Actor
9
+ class Heartbeat < Legion::Extensions::Actors::Every
10
+ def runner_class
11
+ Legion::Extensions::Mesh::Runners::Mesh
12
+ end
13
+
14
+ def runner_function
15
+ 'heartbeat'
16
+ end
17
+
18
+ def time
19
+ 10
20
+ end
21
+
22
+ def run_now?
23
+ true
24
+ end
25
+
26
+ def use_runner?
27
+ false
28
+ end
29
+
30
+ def check_subtask?
31
+ false
32
+ end
33
+
34
+ def generate_task?
35
+ false
36
+ end
37
+
38
+ def args
39
+ { agent_id: Legion::Settings[:client][:name] }
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/mesh/helpers/topology'
4
+ require 'legion/extensions/mesh/helpers/registry'
5
+ require 'legion/extensions/mesh/runners/mesh'
6
+
7
+ module Legion
8
+ module Extensions
9
+ module Mesh
10
+ class Client
11
+ include Runners::Mesh
12
+
13
+ def initialize(**)
14
+ @mesh_registry = Helpers::Registry.new
15
+ end
16
+
17
+ private
18
+
19
+ attr_reader :mesh_registry
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Mesh
6
+ module Helpers
7
+ module PreferenceProfile
8
+ DEFAULTS = {
9
+ verbosity: :normal,
10
+ tone: :professional,
11
+ format: :structured,
12
+ technical_depth: :moderate
13
+ }.freeze
14
+
15
+ VALID_VALUES = {
16
+ 'verbosity' => %i[terse concise normal detailed verbose],
17
+ 'tone' => %i[casual conversational professional formal],
18
+ 'format' => %i[plain structured markdown],
19
+ 'technical_depth' => %i[high_level moderate deep implementation]
20
+ }.freeze
21
+
22
+ SOURCE_CONFIDENCE = {
23
+ 'explicit' => 1.0,
24
+ 'preference_learning' => 0.75,
25
+ 'personality' => 0.4,
26
+ 'defaults' => 0.0
27
+ }.freeze
28
+
29
+ INSTRUCTION_MAP = {
30
+ verbosity: {
31
+ terse: 'Keep responses extremely short — one or two sentences max.',
32
+ concise: 'Keep responses brief and to the point.',
33
+ detailed: 'Provide thorough, detailed responses.',
34
+ verbose: 'Be comprehensive and expansive in your responses.'
35
+ },
36
+ tone: {
37
+ casual: 'Use casual, friendly language.',
38
+ conversational: 'Use a warm, conversational tone.',
39
+ formal: 'Use formal, professional language.'
40
+ },
41
+ format: {
42
+ plain: 'Use plain text without formatting.',
43
+ markdown: 'Use rich markdown formatting with headers and code blocks.'
44
+ },
45
+ technical_depth: {
46
+ high_level: 'Keep explanations high-level, avoid implementation details.',
47
+ deep: 'Include technical depth and implementation details.',
48
+ implementation: 'Include implementation details, code examples, and edge cases.'
49
+ }
50
+ }.freeze
51
+
52
+ module_function
53
+
54
+ def resolve(owner_id:, overrides: nil, personality: nil)
55
+ prefs = DEFAULTS.dup
56
+ sources = Set.new([:defaults])
57
+ custom = {}
58
+
59
+ traces = fetch_preference_traces(owner_id: owner_id)
60
+ all_prefs = traces + (overrides || [])
61
+
62
+ all_prefs.sort_by { |p| p[:confidence] || SOURCE_CONFIDENCE.fetch(p[:source].to_s, 0.0) }.each do |pref|
63
+ domain = pref[:domain].to_s
64
+ value = pref[:value].to_s
65
+ source = pref[:source].to_s
66
+
67
+ if domain.start_with?('custom:')
68
+ custom_key = domain.sub('custom:', '')
69
+ custom[custom_key] = value
70
+ sources << source.to_sym
71
+ elsif VALID_VALUES.key?(domain) && VALID_VALUES[domain].include?(value.to_sym)
72
+ prefs[domain.to_sym] = value.to_sym
73
+ sources << source.to_sym
74
+ end
75
+ end
76
+
77
+ prefs.merge(
78
+ personality: personality,
79
+ custom: custom,
80
+ sources: sources.to_a,
81
+ resolved_at: Time.now
82
+ )
83
+ end
84
+
85
+ def store_preference(owner_id:, domain:, value:, source: 'explicit')
86
+ return { stored: false, reason: :memory_not_available } unless memory_available?
87
+
88
+ confidence = SOURCE_CONFIDENCE.fetch(source.to_s, 0.5)
89
+ memory_runner.store_trace(
90
+ type: :semantic,
91
+ content_payload: { domain: domain, value: value, source: source }.to_s,
92
+ domain_tags: ['preference', "preference:#{domain}", "owner:#{owner_id}"],
93
+ origin: :direct_experience,
94
+ confidence: confidence
95
+ )
96
+ { stored: true, domain: domain, value: value, source: source }
97
+ rescue StandardError => e
98
+ { stored: false, reason: :error, message: e.message }
99
+ end
100
+
101
+ def clear_preferences(owner_id:, source: nil)
102
+ return { cleared: false, reason: :memory_not_available } unless memory_available?
103
+
104
+ { cleared: true, owner_id: owner_id, source: source }
105
+ end
106
+
107
+ def preference_instructions(profile:)
108
+ lines = []
109
+
110
+ INSTRUCTION_MAP.each do |domain, value_map|
111
+ current = profile[domain]
112
+ default = DEFAULTS[domain]
113
+ next if current == default
114
+
115
+ instruction = value_map[current]
116
+ lines << instruction if instruction
117
+ end
118
+
119
+ return nil if lines.empty?
120
+
121
+ lines.join(' ')
122
+ end
123
+
124
+ def memory_available?
125
+ defined?(Legion::Extensions::Memory::Runners::Traces)
126
+ end
127
+
128
+ def memory_runner
129
+ Object.new.extend(Legion::Extensions::Memory::Runners::Traces)
130
+ end
131
+
132
+ def fetch_preference_traces(owner_id:)
133
+ return [] unless memory_available?
134
+
135
+ traces = memory_runner.retrieve_by_domain(domain_tag: "owner:#{owner_id}")
136
+ traces.select { |t| t[:domain_tags]&.include?('preference') }.filter_map do |trace|
137
+ parse_preference_trace(trace)
138
+ end
139
+ rescue StandardError
140
+ []
141
+ end
142
+
143
+ def parse_preference_trace(trace)
144
+ payload = trace[:content_payload]
145
+ return nil unless payload.is_a?(String)
146
+
147
+ match = payload.match(/domain.*?(\w+).*?value.*?(\w+).*?source.*?(\w+)/)
148
+ return nil unless match
149
+
150
+ { domain: match[1], value: match[2], source: match[3], confidence: trace[:confidence] }
151
+ rescue StandardError
152
+ nil
153
+ end
154
+ end
155
+ end
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Mesh
6
+ module Helpers
7
+ class Registry
8
+ attr_reader :agents, :capabilities, :messages
9
+
10
+ def initialize
11
+ @agents = {}
12
+ @capabilities = Hash.new { |h, k| h[k] = [] }
13
+ @messages = []
14
+ end
15
+
16
+ def register_agent(agent_id, capabilities: [], endpoint: nil)
17
+ @agents[agent_id] = {
18
+ agent_id: agent_id,
19
+ capabilities: capabilities,
20
+ endpoint: endpoint,
21
+ registered_at: Time.now.utc,
22
+ last_seen: Time.now.utc,
23
+ status: :online
24
+ }
25
+ capabilities.each { |cap| @capabilities[cap] << agent_id }
26
+ end
27
+
28
+ def unregister_agent(agent_id)
29
+ agent = @agents.delete(agent_id)
30
+ return nil unless agent
31
+
32
+ agent[:capabilities].each { |cap| @capabilities[cap].delete(agent_id) }
33
+ agent
34
+ end
35
+
36
+ def heartbeat(agent_id)
37
+ agent = @agents[agent_id]
38
+ return nil unless agent
39
+
40
+ agent[:last_seen] = Time.now.utc
41
+ agent[:status] = :online
42
+ end
43
+
44
+ def find_by_capability(capability)
45
+ agent_ids = @capabilities[capability] || []
46
+ agent_ids.filter_map { |id| @agents[id] }
47
+ end
48
+
49
+ def route_message(from:, to: nil, capability: nil, pattern: :unicast, payload: {})
50
+ msg = {
51
+ from: from, to: to, capability: capability,
52
+ pattern: pattern, payload: payload, at: Time.now.utc
53
+ }
54
+
55
+ targets = case pattern
56
+ when :unicast then to ? [@agents[to]].compact : []
57
+ when :multicast then capability ? find_by_capability(capability) : []
58
+ when :broadcast then @agents.values
59
+ else []
60
+ end
61
+
62
+ msg[:delivered_to] = targets.map { |t| t[:agent_id] }
63
+ @messages << msg
64
+ @messages.shift while @messages.size > 1000
65
+ msg
66
+ end
67
+
68
+ def online_agents
69
+ @agents.values.select { |a| a[:status] == :online }
70
+ end
71
+
72
+ def count
73
+ @agents.size
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Mesh
6
+ module Helpers
7
+ module Topology
8
+ PROTOCOLS = %i[grpc websocket rest].freeze
9
+ PATTERNS = %i[unicast multicast broadcast].freeze
10
+
11
+ MESH_SILENCE_TIMEOUT = 30 # seconds
12
+ TRUST_CONSIDER_THRESHOLD = 0.3
13
+ MAX_HOPS = 3
14
+
15
+ module_function
16
+
17
+ def valid_protocol?(protocol)
18
+ PROTOCOLS.include?(protocol)
19
+ end
20
+
21
+ def valid_pattern?(pattern)
22
+ PATTERNS.include?(pattern)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Mesh
6
+ module Runners
7
+ module Mesh
8
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
9
+ Legion::Extensions::Helpers.const_defined?(:Lex)
10
+
11
+ def register(agent_id:, capabilities: [], endpoint: nil, **)
12
+ mesh_registry.register_agent(agent_id, capabilities: capabilities, endpoint: endpoint)
13
+ Legion::Logging.info "[mesh] registered: agent=#{agent_id} capabilities=#{capabilities.join(',')}"
14
+ { registered: true, agent_id: agent_id }
15
+ end
16
+
17
+ def unregister(agent_id:, **)
18
+ result = mesh_registry.unregister_agent(agent_id)
19
+ if result
20
+ Legion::Logging.info "[mesh] unregistered: agent=#{agent_id}"
21
+ { unregistered: true }
22
+ else
23
+ Legion::Logging.debug "[mesh] unregister failed: agent=#{agent_id} not found"
24
+ { error: :not_found }
25
+ end
26
+ end
27
+
28
+ def heartbeat(agent_id:, **)
29
+ result = mesh_registry.heartbeat(agent_id)
30
+ Legion::Logging.debug "[mesh] heartbeat: agent=#{agent_id} alive=#{!result.nil?}"
31
+ result ? { alive: true } : { error: :not_registered }
32
+ end
33
+
34
+ def send_message(from:, to: nil, capability: nil, pattern: :unicast, payload: {}, **) # rubocop:disable Metrics/ParameterLists
35
+ return { error: :invalid_pattern } unless Helpers::Topology.valid_pattern?(pattern)
36
+
37
+ msg = mesh_registry.route_message(from: from, to: to, capability: capability,
38
+ pattern: pattern, payload: payload)
39
+ count = msg[:delivered_to].size
40
+ Legion::Logging.debug "[mesh] message: from=#{from} pattern=#{pattern} delivered=#{count} to=#{msg[:delivered_to].join(',')}"
41
+ { sent: true, delivered_to: msg[:delivered_to], count: count }
42
+ end
43
+
44
+ def find_agents(capability:, **)
45
+ agents = mesh_registry.find_by_capability(capability)
46
+ Legion::Logging.debug "[mesh] find: capability=#{capability} found=#{agents.size}"
47
+ { agents: agents.map { |a| a[:agent_id] }, count: agents.size }
48
+ end
49
+
50
+ def mesh_status(**)
51
+ online = mesh_registry.online_agents
52
+ total = mesh_registry.count
53
+ msgs = mesh_registry.messages.size
54
+ Legion::Logging.debug "[mesh] status: total=#{total} online=#{online.size} messages=#{msgs}"
55
+ { total: total, online: online.size, message_count: msgs }
56
+ end
57
+
58
+ private
59
+
60
+ def mesh_registry
61
+ @mesh_registry ||= Helpers::Registry.new
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Mesh
6
+ VERSION = '0.1.1'
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/mesh/version'
4
+ require 'legion/extensions/mesh/helpers/topology'
5
+ require 'legion/extensions/mesh/helpers/registry'
6
+ require 'legion/extensions/mesh/helpers/preference_profile'
7
+ require 'legion/extensions/mesh/runners/mesh'
8
+
9
+ module Legion
10
+ module Extensions
11
+ module Mesh
12
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Stub the framework actor base class since legionio gem is not available in test
4
+ module Legion
5
+ module Extensions
6
+ module Actors
7
+ class Every # rubocop:disable Lint/EmptyClass
8
+ end
9
+ end
10
+ end
11
+ end
12
+
13
+ # Intercept the require in the actor file so it doesn't fail
14
+ $LOADED_FEATURES << 'legion/extensions/actors/every'
15
+
16
+ require 'legion/extensions/mesh/actors/heartbeat'
17
+
18
+ RSpec.describe Legion::Extensions::Mesh::Actor::Heartbeat do
19
+ subject(:actor) { described_class.new }
20
+
21
+ describe '#runner_class' do
22
+ it 'returns the Mesh runner module' do
23
+ expect(actor.runner_class).to eq(Legion::Extensions::Mesh::Runners::Mesh)
24
+ end
25
+ end
26
+
27
+ describe '#runner_function' do
28
+ it 'returns heartbeat' do
29
+ expect(actor.runner_function).to eq('heartbeat')
30
+ end
31
+ end
32
+
33
+ describe '#time' do
34
+ it 'returns 10' do
35
+ expect(actor.time).to eq(10)
36
+ end
37
+ end
38
+
39
+ describe '#run_now?' do
40
+ it 'returns true' do
41
+ expect(actor.run_now?).to be true
42
+ end
43
+ end
44
+
45
+ describe '#use_runner?' do
46
+ it 'returns false' do
47
+ expect(actor.use_runner?).to be false
48
+ end
49
+ end
50
+
51
+ describe '#check_subtask?' do
52
+ it 'returns false' do
53
+ expect(actor.check_subtask?).to be false
54
+ end
55
+ end
56
+
57
+ describe '#generate_task?' do
58
+ it 'returns false' do
59
+ expect(actor.generate_task?).to be false
60
+ end
61
+ end
62
+
63
+ describe '#args' do
64
+ it 'returns a hash with agent_id from Legion::Settings' do
65
+ settings = { client: { name: 'test-agent' } }
66
+ stub_const('Legion::Settings', settings)
67
+ expect(actor.args).to eq({ agent_id: 'test-agent' })
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/mesh/client'
4
+
5
+ RSpec.describe Legion::Extensions::Mesh::Client do
6
+ it 'responds to mesh runner methods' do
7
+ client = described_class.new
8
+ expect(client).to respond_to(:register)
9
+ expect(client).to respond_to(:unregister)
10
+ expect(client).to respond_to(:heartbeat)
11
+ expect(client).to respond_to(:send_message)
12
+ expect(client).to respond_to(:find_agents)
13
+ expect(client).to respond_to(:mesh_status)
14
+ end
15
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ # Stub lex-memory if not loaded
6
+ unless defined?(Legion::Extensions::Memory::Runners::Traces)
7
+ module Legion
8
+ module Extensions
9
+ module Memory
10
+ module Runners
11
+ module Traces
12
+ def retrieve_by_domain(**)
13
+ []
14
+ end
15
+
16
+ def store_trace(**)
17
+ { stored: true }
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ RSpec.describe Legion::Extensions::Mesh::Helpers::PreferenceProfile do
27
+ let(:profile_mod) { described_class }
28
+
29
+ describe '.resolve' do
30
+ context 'with no preferences stored' do
31
+ it 'returns default profile' do
32
+ result = profile_mod.resolve(owner_id: 'user1')
33
+ expect(result[:verbosity]).to eq(:normal)
34
+ expect(result[:tone]).to eq(:professional)
35
+ expect(result[:format]).to eq(:structured)
36
+ expect(result[:technical_depth]).to eq(:moderate)
37
+ expect(result[:sources]).to eq([:defaults])
38
+ expect(result[:resolved_at]).to be_a(Time)
39
+ end
40
+ end
41
+
42
+ context 'with explicit preference stored' do
43
+ it 'overrides default for that domain' do
44
+ result = profile_mod.resolve(owner_id: 'user1', overrides: [
45
+ { domain: 'verbosity', value: 'concise', source: 'explicit', confidence: 1.0 }
46
+ ])
47
+ expect(result[:verbosity]).to eq(:concise)
48
+ expect(result[:sources]).to include(:explicit)
49
+ end
50
+ end
51
+
52
+ context 'with multiple sources for same domain' do
53
+ it 'higher confidence wins' do
54
+ result = profile_mod.resolve(owner_id: 'user1', overrides: [
55
+ { domain: 'tone', value: 'casual', source: 'personality', confidence: 0.4 },
56
+ { domain: 'tone', value: 'formal', source: 'explicit', confidence: 1.0 }
57
+ ])
58
+ expect(result[:tone]).to eq(:formal)
59
+ end
60
+ end
61
+
62
+ context 'with personality traits' do
63
+ it 'maps OCEAN traits to preference defaults' do
64
+ result = profile_mod.resolve(
65
+ owner_id: 'user1',
66
+ personality: { openness: 0.9, conscientiousness: 0.3 }
67
+ )
68
+ expect(result[:personality]).to eq({ openness: 0.9, conscientiousness: 0.3 })
69
+ end
70
+ end
71
+
72
+ context 'with custom preferences' do
73
+ it 'includes custom key-value pairs' do
74
+ result = profile_mod.resolve(owner_id: 'user1', overrides: [
75
+ { domain: 'custom:response_language', value: 'es', source: 'explicit', confidence: 1.0 }
76
+ ])
77
+ expect(result[:custom]).to include('response_language' => 'es')
78
+ end
79
+ end
80
+ end
81
+
82
+ describe '.store_preference' do
83
+ it 'returns stored result with trace data' do
84
+ result = profile_mod.store_preference(
85
+ owner_id: 'user1', domain: 'verbosity', value: 'concise', source: 'explicit'
86
+ )
87
+ expect(result[:stored]).to be true
88
+ expect(result[:domain]).to eq('verbosity')
89
+ end
90
+
91
+ it 'returns not_available when lex-memory is not loaded' do
92
+ allow(profile_mod).to receive(:memory_available?).and_return(false)
93
+ result = profile_mod.store_preference(
94
+ owner_id: 'user1', domain: 'verbosity', value: 'concise', source: 'explicit'
95
+ )
96
+ expect(result[:stored]).to be false
97
+ expect(result[:reason]).to eq(:memory_not_available)
98
+ end
99
+ end
100
+
101
+ describe '.clear_preferences' do
102
+ it 'returns cleared result' do
103
+ result = profile_mod.clear_preferences(owner_id: 'user1', source: 'explicit')
104
+ expect(result[:cleared]).to be true
105
+ end
106
+ end
107
+
108
+ describe '.preference_instructions' do
109
+ it 'generates natural language prompt instructions from profile' do
110
+ profile = {
111
+ verbosity: :concise,
112
+ tone: :formal,
113
+ format: :structured,
114
+ technical_depth: :deep
115
+ }
116
+ instructions = profile_mod.preference_instructions(profile: profile)
117
+ expect(instructions).to include('brief')
118
+ expect(instructions).to include('formal')
119
+ expect(instructions).to include('implementation')
120
+ end
121
+
122
+ it 'returns nil for all-default profile' do
123
+ profile = { verbosity: :normal, tone: :professional, format: :structured, technical_depth: :moderate }
124
+ instructions = profile_mod.preference_instructions(profile: profile)
125
+ expect(instructions).to be_nil
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,292 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Mesh::Helpers::Registry do
4
+ let(:registry) { described_class.new }
5
+
6
+ describe '#initialize' do
7
+ it 'starts with an empty agents hash' do
8
+ expect(registry.agents).to eq({})
9
+ end
10
+
11
+ it 'starts with an empty capabilities hash' do
12
+ expect(registry.capabilities).to eq({})
13
+ end
14
+
15
+ it 'starts with an empty messages array' do
16
+ expect(registry.messages).to eq([])
17
+ end
18
+ end
19
+
20
+ describe '#register_agent' do
21
+ it 'adds the agent to the agents hash' do
22
+ registry.register_agent('agent-1')
23
+ expect(registry.agents).to have_key('agent-1')
24
+ end
25
+
26
+ it 'stores the agent_id on the record' do
27
+ registry.register_agent('agent-1')
28
+ expect(registry.agents['agent-1'][:agent_id]).to eq('agent-1')
29
+ end
30
+
31
+ it 'defaults status to :online' do
32
+ registry.register_agent('agent-1')
33
+ expect(registry.agents['agent-1'][:status]).to eq(:online)
34
+ end
35
+
36
+ it 'stores an empty capabilities array by default' do
37
+ registry.register_agent('agent-1')
38
+ expect(registry.agents['agent-1'][:capabilities]).to eq([])
39
+ end
40
+
41
+ it 'stores provided capabilities' do
42
+ registry.register_agent('agent-1', capabilities: %i[search code_review])
43
+ expect(registry.agents['agent-1'][:capabilities]).to eq(%i[search code_review])
44
+ end
45
+
46
+ it 'stores the endpoint when provided' do
47
+ registry.register_agent('agent-1', endpoint: 'http://host:9000')
48
+ expect(registry.agents['agent-1'][:endpoint]).to eq('http://host:9000')
49
+ end
50
+
51
+ it 'defaults endpoint to nil' do
52
+ registry.register_agent('agent-1')
53
+ expect(registry.agents['agent-1'][:endpoint]).to be_nil
54
+ end
55
+
56
+ it 'indexes each capability to the agent_id' do
57
+ registry.register_agent('agent-1', capabilities: [:search])
58
+ expect(registry.capabilities[:search]).to include('agent-1')
59
+ end
60
+
61
+ it 'indexes multiple capabilities separately' do
62
+ registry.register_agent('agent-1', capabilities: %i[search compute])
63
+ expect(registry.capabilities[:search]).to include('agent-1')
64
+ expect(registry.capabilities[:compute]).to include('agent-1')
65
+ end
66
+
67
+ it 'accumulates multiple agents under the same capability' do
68
+ registry.register_agent('agent-1', capabilities: [:search])
69
+ registry.register_agent('agent-2', capabilities: [:search])
70
+ expect(registry.capabilities[:search]).to include('agent-1', 'agent-2')
71
+ end
72
+
73
+ it 'records registered_at as a UTC Time' do
74
+ before = Time.now.utc
75
+ registry.register_agent('agent-1')
76
+ after = Time.now.utc
77
+ ts = registry.agents['agent-1'][:registered_at]
78
+ expect(ts).to be_between(before, after)
79
+ end
80
+
81
+ it 'sets last_seen on registration' do
82
+ registry.register_agent('agent-1')
83
+ expect(registry.agents['agent-1'][:last_seen]).not_to be_nil
84
+ end
85
+ end
86
+
87
+ describe '#unregister_agent' do
88
+ before { registry.register_agent('agent-1', capabilities: %i[search compute]) }
89
+
90
+ it 'removes the agent from agents hash' do
91
+ registry.unregister_agent('agent-1')
92
+ expect(registry.agents).not_to have_key('agent-1')
93
+ end
94
+
95
+ it 'returns the removed agent record' do
96
+ result = registry.unregister_agent('agent-1')
97
+ expect(result[:agent_id]).to eq('agent-1')
98
+ end
99
+
100
+ it 'removes the agent from each capability index' do
101
+ registry.unregister_agent('agent-1')
102
+ expect(registry.capabilities[:search]).not_to include('agent-1')
103
+ expect(registry.capabilities[:compute]).not_to include('agent-1')
104
+ end
105
+
106
+ it 'does not affect other agents in the same capability' do
107
+ registry.register_agent('agent-2', capabilities: [:search])
108
+ registry.unregister_agent('agent-1')
109
+ expect(registry.capabilities[:search]).to include('agent-2')
110
+ end
111
+
112
+ it 'returns nil for unknown agent_id' do
113
+ expect(registry.unregister_agent('nonexistent')).to be_nil
114
+ end
115
+
116
+ it 'leaves other agents unaffected' do
117
+ registry.register_agent('agent-2')
118
+ registry.unregister_agent('agent-1')
119
+ expect(registry.agents).to have_key('agent-2')
120
+ end
121
+ end
122
+
123
+ describe '#heartbeat' do
124
+ before { registry.register_agent('agent-1') }
125
+
126
+ it 'updates last_seen to a recent UTC time' do
127
+ before = Time.now.utc
128
+ registry.heartbeat('agent-1')
129
+ after = Time.now.utc
130
+ expect(registry.agents['agent-1'][:last_seen]).to be_between(before, after)
131
+ end
132
+
133
+ it 'sets status to :online' do
134
+ registry.agents['agent-1'][:status] = :unknown
135
+ registry.heartbeat('agent-1')
136
+ expect(registry.agents['agent-1'][:status]).to eq(:online)
137
+ end
138
+
139
+ it 'returns nil for unknown agent_id' do
140
+ expect(registry.heartbeat('nonexistent')).to be_nil
141
+ end
142
+ end
143
+
144
+ describe '#find_by_capability' do
145
+ it 'returns agents that have the requested capability' do
146
+ registry.register_agent('a1', capabilities: [:search])
147
+ registry.register_agent('a2', capabilities: [:search])
148
+ results = registry.find_by_capability(:search)
149
+ ids = results.map { |a| a[:agent_id] }
150
+ expect(ids).to contain_exactly('a1', 'a2')
151
+ end
152
+
153
+ it 'returns only agents with that specific capability' do
154
+ registry.register_agent('a1', capabilities: [:search])
155
+ registry.register_agent('a2', capabilities: [:compute])
156
+ results = registry.find_by_capability(:search)
157
+ expect(results.size).to eq(1)
158
+ expect(results.first[:agent_id]).to eq('a1')
159
+ end
160
+
161
+ it 'returns an empty array when no agents have the capability' do
162
+ expect(registry.find_by_capability(:nonexistent)).to eq([])
163
+ end
164
+
165
+ it 'excludes agents that were unregistered' do
166
+ registry.register_agent('a1', capabilities: [:search])
167
+ registry.unregister_agent('a1')
168
+ expect(registry.find_by_capability(:search)).to eq([])
169
+ end
170
+ end
171
+
172
+ describe '#route_message' do
173
+ before do
174
+ registry.register_agent('a1', capabilities: [:search])
175
+ registry.register_agent('a2', capabilities: [:search])
176
+ registry.register_agent('a3', capabilities: [:compute])
177
+ end
178
+
179
+ describe 'unicast pattern' do
180
+ it 'delivers to the specified recipient' do
181
+ result = registry.route_message(from: 'sender', to: 'a1', pattern: :unicast, payload: { x: 1 })
182
+ expect(result[:delivered_to]).to eq(['a1'])
183
+ end
184
+
185
+ it 'returns empty delivered_to for unknown recipient' do
186
+ result = registry.route_message(from: 'sender', to: 'unknown', pattern: :unicast)
187
+ expect(result[:delivered_to]).to eq([])
188
+ end
189
+
190
+ it 'records the message in the buffer' do
191
+ registry.route_message(from: 'sender', to: 'a1', pattern: :unicast)
192
+ expect(registry.messages.size).to eq(1)
193
+ end
194
+
195
+ it 'stores from, to, pattern, and payload on the message' do
196
+ registry.route_message(from: 'sender', to: 'a1', pattern: :unicast, payload: { key: 'val' })
197
+ msg = registry.messages.last
198
+ expect(msg[:from]).to eq('sender')
199
+ expect(msg[:to]).to eq('a1')
200
+ expect(msg[:pattern]).to eq(:unicast)
201
+ expect(msg[:payload]).to eq({ key: 'val' })
202
+ end
203
+ end
204
+
205
+ describe 'multicast pattern' do
206
+ it 'delivers to all agents with the given capability' do
207
+ result = registry.route_message(from: 'sender', capability: :search, pattern: :multicast)
208
+ expect(result[:delivered_to]).to contain_exactly('a1', 'a2')
209
+ end
210
+
211
+ it 'returns empty delivered_to when capability has no agents' do
212
+ result = registry.route_message(from: 'sender', capability: :unknown, pattern: :multicast)
213
+ expect(result[:delivered_to]).to eq([])
214
+ end
215
+
216
+ it 'returns empty delivered_to when no capability is given' do
217
+ result = registry.route_message(from: 'sender', pattern: :multicast)
218
+ expect(result[:delivered_to]).to eq([])
219
+ end
220
+ end
221
+
222
+ describe 'broadcast pattern' do
223
+ it 'delivers to all registered agents' do
224
+ result = registry.route_message(from: 'sender', pattern: :broadcast)
225
+ expect(result[:delivered_to]).to contain_exactly('a1', 'a2', 'a3')
226
+ end
227
+
228
+ it 'returns empty delivered_to when no agents registered' do
229
+ empty = described_class.new
230
+ result = empty.route_message(from: 'sender', pattern: :broadcast)
231
+ expect(result[:delivered_to]).to eq([])
232
+ end
233
+ end
234
+
235
+ describe 'unknown pattern' do
236
+ it 'returns empty delivered_to' do
237
+ result = registry.route_message(from: 'sender', pattern: :unknown_pattern)
238
+ expect(result[:delivered_to]).to eq([])
239
+ end
240
+ end
241
+
242
+ describe 'message buffer cap' do
243
+ it 'shifts old messages when buffer exceeds 1000' do
244
+ 1001.times { |i| registry.route_message(from: 'sender', to: "a#{i}", pattern: :unicast) }
245
+ expect(registry.messages.size).to eq(1000)
246
+ end
247
+ end
248
+
249
+ it 'stores a timestamp on the message' do
250
+ before = Time.now.utc
251
+ registry.route_message(from: 'sender', pattern: :broadcast)
252
+ after = Time.now.utc
253
+ expect(registry.messages.last[:at]).to be_between(before, after)
254
+ end
255
+ end
256
+
257
+ describe '#online_agents' do
258
+ it 'returns agents with :online status' do
259
+ registry.register_agent('a1')
260
+ registry.register_agent('a2')
261
+ expect(registry.online_agents.size).to eq(2)
262
+ end
263
+
264
+ it 'excludes agents with non-online status' do
265
+ registry.register_agent('a1')
266
+ registry.agents['a1'][:status] = :offline
267
+ expect(registry.online_agents).to eq([])
268
+ end
269
+
270
+ it 'returns an empty array when no agents are registered' do
271
+ expect(registry.online_agents).to eq([])
272
+ end
273
+ end
274
+
275
+ describe '#count' do
276
+ it 'returns 0 for an empty registry' do
277
+ expect(registry.count).to eq(0)
278
+ end
279
+
280
+ it 'returns the number of registered agents' do
281
+ registry.register_agent('a1')
282
+ registry.register_agent('a2')
283
+ expect(registry.count).to eq(2)
284
+ end
285
+
286
+ it 'decrements after unregistration' do
287
+ registry.register_agent('a1')
288
+ registry.unregister_agent('a1')
289
+ expect(registry.count).to eq(0)
290
+ end
291
+ end
292
+ end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Legion::Extensions::Mesh::Helpers::Topology do
4
+ describe 'constants' do
5
+ it 'defines PROTOCOLS as a frozen array of symbols' do
6
+ expect(described_class::PROTOCOLS).to eq(%i[grpc websocket rest])
7
+ expect(described_class::PROTOCOLS).to be_frozen
8
+ end
9
+
10
+ it 'defines PATTERNS as a frozen array of symbols' do
11
+ expect(described_class::PATTERNS).to eq(%i[unicast multicast broadcast])
12
+ expect(described_class::PATTERNS).to be_frozen
13
+ end
14
+
15
+ it 'defines MESH_SILENCE_TIMEOUT as 30' do
16
+ expect(described_class::MESH_SILENCE_TIMEOUT).to eq(30)
17
+ end
18
+
19
+ it 'defines TRUST_CONSIDER_THRESHOLD as 0.3' do
20
+ expect(described_class::TRUST_CONSIDER_THRESHOLD).to eq(0.3)
21
+ end
22
+
23
+ it 'defines MAX_HOPS as 3' do
24
+ expect(described_class::MAX_HOPS).to eq(3)
25
+ end
26
+ end
27
+
28
+ describe '.valid_protocol?' do
29
+ it 'returns true for :grpc' do
30
+ expect(described_class.valid_protocol?(:grpc)).to be true
31
+ end
32
+
33
+ it 'returns true for :websocket' do
34
+ expect(described_class.valid_protocol?(:websocket)).to be true
35
+ end
36
+
37
+ it 'returns true for :rest' do
38
+ expect(described_class.valid_protocol?(:rest)).to be true
39
+ end
40
+
41
+ it 'returns false for an unknown protocol' do
42
+ expect(described_class.valid_protocol?(:http2)).to be false
43
+ end
44
+
45
+ it 'returns false for nil' do
46
+ expect(described_class.valid_protocol?(nil)).to be false
47
+ end
48
+
49
+ it 'returns false for a string version of a valid protocol' do
50
+ expect(described_class.valid_protocol?('grpc')).to be false
51
+ end
52
+
53
+ it 'accepts all PROTOCOLS members as valid' do
54
+ described_class::PROTOCOLS.each do |proto|
55
+ expect(described_class.valid_protocol?(proto)).to be true
56
+ end
57
+ end
58
+ end
59
+
60
+ describe '.valid_pattern?' do
61
+ it 'returns true for :unicast' do
62
+ expect(described_class.valid_pattern?(:unicast)).to be true
63
+ end
64
+
65
+ it 'returns true for :multicast' do
66
+ expect(described_class.valid_pattern?(:multicast)).to be true
67
+ end
68
+
69
+ it 'returns true for :broadcast' do
70
+ expect(described_class.valid_pattern?(:broadcast)).to be true
71
+ end
72
+
73
+ it 'returns false for an unknown pattern' do
74
+ expect(described_class.valid_pattern?(:anycast)).to be false
75
+ end
76
+
77
+ it 'returns false for nil' do
78
+ expect(described_class.valid_pattern?(nil)).to be false
79
+ end
80
+
81
+ it 'returns false for a string version of a valid pattern' do
82
+ expect(described_class.valid_pattern?('unicast')).to be false
83
+ end
84
+
85
+ it 'accepts all PATTERNS members as valid' do
86
+ described_class::PATTERNS.each do |pat|
87
+ expect(described_class.valid_pattern?(pat)).to be true
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/mesh/client'
4
+
5
+ RSpec.describe Legion::Extensions::Mesh::Runners::Mesh do
6
+ let(:client) { Legion::Extensions::Mesh::Client.new }
7
+
8
+ describe '#register' do
9
+ it 'registers an agent' do
10
+ result = client.register(agent_id: 'agent-1', capabilities: [:search])
11
+ expect(result[:registered]).to be true
12
+ end
13
+ end
14
+
15
+ describe '#unregister' do
16
+ it 'unregisters a known agent' do
17
+ client.register(agent_id: 'agent-1')
18
+ result = client.unregister(agent_id: 'agent-1')
19
+ expect(result[:unregistered]).to be true
20
+ end
21
+
22
+ it 'returns error for unknown agent' do
23
+ result = client.unregister(agent_id: 'unknown')
24
+ expect(result[:error]).to eq(:not_found)
25
+ end
26
+ end
27
+
28
+ describe '#send_message' do
29
+ it 'sends unicast message' do
30
+ client.register(agent_id: 'agent-1')
31
+ result = client.send_message(from: 'agent-2', to: 'agent-1', payload: { text: 'hello' })
32
+ expect(result[:sent]).to be true
33
+ expect(result[:count]).to eq(1)
34
+ end
35
+
36
+ it 'sends multicast by capability' do
37
+ client.register(agent_id: 'agent-1', capabilities: [:search])
38
+ client.register(agent_id: 'agent-2', capabilities: [:search])
39
+ client.register(agent_id: 'agent-3', capabilities: [:compute])
40
+ result = client.send_message(from: 'x', capability: :search, pattern: :multicast)
41
+ expect(result[:count]).to eq(2)
42
+ end
43
+
44
+ it 'sends broadcast' do
45
+ client.register(agent_id: 'a1')
46
+ client.register(agent_id: 'a2')
47
+ result = client.send_message(from: 'x', pattern: :broadcast)
48
+ expect(result[:count]).to eq(2)
49
+ end
50
+ end
51
+
52
+ describe '#find_agents' do
53
+ it 'finds agents by capability' do
54
+ client.register(agent_id: 'a1', capabilities: [:code_review])
55
+ result = client.find_agents(capability: :code_review)
56
+ expect(result[:count]).to eq(1)
57
+ end
58
+ end
59
+
60
+ describe '#mesh_status' do
61
+ it 'returns status' do
62
+ client.register(agent_id: 'a1')
63
+ status = client.mesh_status
64
+ expect(status[:total]).to eq(1)
65
+ expect(status[:online]).to eq(1)
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+
5
+ module Legion
6
+ module Logging
7
+ def self.debug(_msg); end
8
+ def self.info(_msg); end
9
+ def self.warn(_msg); end
10
+ def self.error(_msg); end
11
+ end
12
+ end
13
+
14
+ require 'legion/extensions/mesh'
15
+
16
+ RSpec.configure do |config|
17
+ config.example_status_persistence_file_path = '.rspec_status'
18
+ config.disable_monkey_patching!
19
+ config.expect_with(:rspec) { |c| c.syntax = :expect }
20
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lex-mesh
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.1
5
+ platform: ruby
6
+ authors:
7
+ - Esity
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: legion-gaia
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :development
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ description: Agent-to-agent mesh communication protocol for brain-modeled agentic
27
+ AI
28
+ email:
29
+ - matthewdiverson@gmail.com
30
+ executables: []
31
+ extensions: []
32
+ extra_rdoc_files: []
33
+ files:
34
+ - Gemfile
35
+ - lex-mesh.gemspec
36
+ - lib/legion/extensions/mesh.rb
37
+ - lib/legion/extensions/mesh/actors/heartbeat.rb
38
+ - lib/legion/extensions/mesh/client.rb
39
+ - lib/legion/extensions/mesh/helpers/preference_profile.rb
40
+ - lib/legion/extensions/mesh/helpers/registry.rb
41
+ - lib/legion/extensions/mesh/helpers/topology.rb
42
+ - lib/legion/extensions/mesh/runners/mesh.rb
43
+ - lib/legion/extensions/mesh/version.rb
44
+ - spec/legion/extensions/mesh/actors/heartbeat_spec.rb
45
+ - spec/legion/extensions/mesh/client_spec.rb
46
+ - spec/legion/extensions/mesh/helpers/preference_profile_spec.rb
47
+ - spec/legion/extensions/mesh/helpers/registry_spec.rb
48
+ - spec/legion/extensions/mesh/helpers/topology_spec.rb
49
+ - spec/legion/extensions/mesh/runners/mesh_spec.rb
50
+ - spec/spec_helper.rb
51
+ homepage: https://github.com/LegionIO/lex-mesh
52
+ licenses:
53
+ - MIT
54
+ metadata:
55
+ homepage_uri: https://github.com/LegionIO/lex-mesh
56
+ source_code_uri: https://github.com/LegionIO/lex-mesh
57
+ documentation_uri: https://github.com/LegionIO/lex-mesh
58
+ changelog_uri: https://github.com/LegionIO/lex-mesh
59
+ bug_tracker_uri: https://github.com/LegionIO/lex-mesh/issues
60
+ rubygems_mfa_required: 'true'
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '3.4'
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements: []
75
+ rubygems_version: 3.6.9
76
+ specification_version: 4
77
+ summary: LEX Mesh
78
+ test_files: []