lex-mesh 0.2.5 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 650958df48e1ad4fa390b3fecb3ab2817dc535c279449dc4ce114e92f59cfff2
4
- data.tar.gz: ff13ef5a0e18147daa15b72579de24f3691bec50f5f124627d2b503208cfba82
3
+ metadata.gz: 85f99c905c7878c400b57d6d7e1868838521ab8d2f7f5fad973d6a813c09f411
4
+ data.tar.gz: b567bb9e7d1defaf1224aa3184fe63c80bc20458fce0d70fb936251e81247634
5
5
  SHA512:
6
- metadata.gz: fc982147fb2a22e4a5bb40fa8f7a767e87aed754f59474d4937059a831adcfeae6b4f4e4c9add47359c3b6a69200a0cdea21c1bafd1b00dfd74501c93415eb40
7
- data.tar.gz: e34a1a0d9ae8481bb40949bda0e47d6e1647856777214d133acf10c121b8e70962bb85c10fda16120b9dd7bc958828fc20a73851ab259c89fa813d7df2236f26
6
+ metadata.gz: a7b28d8162556724cae30e898607e38633ad7749d98d10a273188c67c203517fb8aff0fa7408e365c2808f1e4dfc95bca5625077187ce7c814cc6ee30e2d5053
7
+ data.tar.gz: e0e21868a89d6856b83c92d3034aa85d6ba9a1cda02f8afc62a8921c434211e05ad2764395c03b30f3439ca1eb89aa7ae3f616a07e15988b0af28932713f80c2
data/lex-mesh.gemspec CHANGED
@@ -28,4 +28,12 @@ Gem::Specification.new do |spec|
28
28
 
29
29
  spec.add_dependency 'base64'
30
30
  spec.add_dependency 'ed25519', '~> 1.3'
31
+
32
+ spec.add_dependency 'legion-cache', '>= 1.3.11'
33
+ spec.add_dependency 'legion-crypt', '>= 1.4.9'
34
+ spec.add_dependency 'legion-data', '>= 1.4.17'
35
+ spec.add_dependency 'legion-json', '>= 1.2.1'
36
+ spec.add_dependency 'legion-logging', '>= 1.3.2'
37
+ spec.add_dependency 'legion-settings', '>= 1.3.14'
38
+ spec.add_dependency 'legion-transport', '>= 1.3.9'
31
39
  end
@@ -2,6 +2,8 @@
2
2
 
3
3
  require 'legion/extensions/mesh/runners/mesh'
4
4
  require 'legion/extensions/mesh/runners/preferences'
5
+ require 'legion/extensions/mesh/runners/delegation'
6
+ require 'legion/extensions/mesh/runners/task_request'
5
7
  require 'legion/extensions/mesh/helpers/preference_profile'
6
8
  require 'legion/extensions/mesh/helpers/pending_requests'
7
9
  require 'legion/extensions/mesh/helpers/topology'
@@ -13,6 +15,8 @@ module Legion
13
15
  class Client
14
16
  include Runners::Mesh
15
17
  include Runners::Preferences
18
+ include Runners::Delegation
19
+ include Runners::TaskRequest
16
20
 
17
21
  def initialize(**opts)
18
22
  @opts = opts
@@ -66,7 +66,7 @@ module Legion
66
66
  detail: { to: record[:to_agent_id], depth: record[:depth], consent: record[:consent_level] }
67
67
  ).publish
68
68
  rescue StandardError => e
69
- Legion::Logging.warn "[mesh] failed to publish #{event_type}: #{e.message}" if defined?(Legion::Logging)
69
+ log.warn "[mesh] failed to publish #{event_type}: #{e.message}"
70
70
  end
71
71
 
72
72
  def delegation_tracker
@@ -10,25 +10,25 @@ module Legion
10
10
 
11
11
  def register(agent_id:, capabilities: [], endpoint: nil, **)
12
12
  mesh_registry.register_agent(agent_id, capabilities: capabilities, endpoint: endpoint)
13
- Legion::Logging.info "[mesh] registered: agent=#{agent_id} capabilities=#{capabilities.join(',')}"
13
+ log.info "[mesh] registered: agent=#{agent_id} capabilities=#{capabilities.join(',')}"
14
14
  { registered: true, agent_id: agent_id }
15
15
  end
16
16
 
17
17
  def unregister(agent_id:, **)
18
18
  result = mesh_registry.unregister_agent(agent_id)
19
19
  if result
20
- Legion::Logging.info "[mesh] unregistered: agent=#{agent_id}"
20
+ log.info "[mesh] unregistered: agent=#{agent_id}"
21
21
  publish_mesh_departure(agent_id: agent_id, capabilities: result[:capabilities] || [])
22
22
  { unregistered: true }
23
23
  else
24
- Legion::Logging.debug "[mesh] unregister failed: agent=#{agent_id} not found"
24
+ log.debug "[mesh] unregister failed: agent=#{agent_id} not found"
25
25
  { error: :not_found }
26
26
  end
27
27
  end
28
28
 
29
29
  def heartbeat(agent_id:, **)
30
30
  result = mesh_registry.heartbeat(agent_id)
31
- Legion::Logging.debug "[mesh] heartbeat: agent=#{agent_id} alive=#{!result.nil?}"
31
+ log.debug "[mesh] heartbeat: agent=#{agent_id} alive=#{!result.nil?}"
32
32
  result ? { alive: true } : { error: :not_registered }
33
33
  end
34
34
 
@@ -38,13 +38,13 @@ module Legion
38
38
  msg = mesh_registry.route_message(from: from, to: to, capability: capability,
39
39
  pattern: pattern, payload: payload)
40
40
  count = msg[:delivered_to].size
41
- Legion::Logging.debug "[mesh] message: from=#{from} pattern=#{pattern} delivered=#{count} to=#{msg[:delivered_to].join(',')}"
41
+ log.debug "[mesh] message: from=#{from} pattern=#{pattern} delivered=#{count} to=#{msg[:delivered_to].join(',')}"
42
42
  { sent: true, delivered_to: msg[:delivered_to], count: count }
43
43
  end
44
44
 
45
45
  def find_agents(capability:, **)
46
46
  agents = mesh_registry.find_by_capability(capability)
47
- Legion::Logging.debug "[mesh] find: capability=#{capability} found=#{agents.size}"
47
+ log.debug "[mesh] find: capability=#{capability} found=#{agents.size}"
48
48
  { agents: agents.map { |a| a[:agent_id] }, count: agents.size }
49
49
  end
50
50
 
@@ -52,14 +52,14 @@ module Legion
52
52
  online = mesh_registry.online_agents
53
53
  total = mesh_registry.count
54
54
  msgs = mesh_registry.messages.size
55
- Legion::Logging.debug "[mesh] status: total=#{total} online=#{online.size} messages=#{msgs}"
55
+ log.debug "[mesh] status: total=#{total} online=#{online.size} messages=#{msgs}"
56
56
  { total: total, online: online.size, message_count: msgs }
57
57
  end
58
58
 
59
59
  def expire_silent_agents(**)
60
60
  expired = mesh_registry.expire_silent_agents
61
61
  expired.each do |agent_id|
62
- Legion::Logging.info "[mesh] expired silent agent: #{agent_id}"
62
+ log.info "[mesh] expired silent agent: #{agent_id}"
63
63
  end
64
64
  { success: true, expired: expired, count: expired.size }
65
65
  end
@@ -116,9 +116,9 @@ module Legion
116
116
  Legion::Extensions::Mesh::Transport::Messages::MeshDeparture.new(
117
117
  agent_id: agent_id, capabilities: capabilities
118
118
  ).publish
119
- Legion::Logging.debug "[mesh] departure signal published: agent=#{agent_id}"
119
+ log.debug "[mesh] departure signal published: agent=#{agent_id}"
120
120
  rescue StandardError => e
121
- Legion::Logging.warn "[mesh] failed to publish departure signal: #{e.message}"
121
+ log.warn "[mesh] failed to publish departure signal: #{e.message}"
122
122
  end
123
123
 
124
124
  def publish_gossip_message(peers)
@@ -75,10 +75,12 @@ module Legion
75
75
 
76
76
  def local_agent_id
77
77
  if defined?(Legion::Settings)
78
- Legion::Settings['client']['name']
78
+ Legion::Settings[:client][:name]
79
79
  else
80
80
  'unknown'
81
81
  end
82
+ rescue StandardError
83
+ 'unknown'
82
84
  end
83
85
 
84
86
  def publish_preference_response(msg, profile)
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require_relative '../helpers/pending_requests'
5
+ require_relative '../helpers/delegation'
6
+ require_relative '../helpers/registry'
7
+ require_relative 'mesh'
8
+
9
+ module Legion
10
+ module Extensions
11
+ module Mesh
12
+ module Runners
13
+ module TaskRequest
14
+ include Runners::Mesh
15
+
16
+ DEFAULT_TIMEOUT = 30
17
+
18
+ def request_task(from:, to:, task:, payload:, timeout: DEFAULT_TIMEOUT, consent_level: :execute, **) # rubocop:disable Metrics/ParameterLists
19
+ target_id = resolve_target(to)
20
+ return { success: false, reason: :no_agent_found, requested: to } unless target_id
21
+
22
+ correlation_id = "task-#{SecureRandom.uuid}"
23
+
24
+ delegation = delegation_tracker.create(
25
+ from: from, to: target_id,
26
+ task_context: task, consent_level: consent_level
27
+ )
28
+ return { success: false, reason: delegation[:error] } if delegation[:error]
29
+
30
+ task_pending.register(
31
+ correlation_id: correlation_id,
32
+ callback: nil,
33
+ ttl: timeout
34
+ )
35
+
36
+ send_message(from: from, to: target_id, pattern: :unicast,
37
+ payload: { type: :task_request, correlation_id: correlation_id,
38
+ task: task, payload: payload, reply_to: from })
39
+
40
+ log.info "[mesh-task] request: from=#{from} to=#{target_id} task=#{task} cid=#{correlation_id[0..11]}"
41
+ { success: true, correlation_id: correlation_id, delegation_id: delegation[:delegation_id],
42
+ target_agent: target_id }
43
+ end
44
+
45
+ def handle_task_reply(correlation_id:, result:, **)
46
+ resolved = task_pending.resolve(correlation_id: correlation_id, result: result)
47
+ if resolved
48
+ log.debug "[mesh-task] reply resolved: cid=#{correlation_id[0..11]}"
49
+ { success: true, resolved: true, correlation_id: correlation_id }
50
+ else
51
+ { success: false, reason: :not_found, correlation_id: correlation_id }
52
+ end
53
+ end
54
+
55
+ def pending_task_stats(**)
56
+ { success: true, pending_count: task_pending.pending_count }
57
+ end
58
+
59
+ def expire_pending_tasks(**)
60
+ expired = task_pending.expire
61
+ log.debug "[mesh-task] expired #{expired.size} pending tasks" unless expired.empty?
62
+ { success: true, expired_count: expired.size, expired_ids: expired }
63
+ end
64
+
65
+ private
66
+
67
+ def resolve_target(to)
68
+ return to if mesh_registry.agents.key?(to)
69
+
70
+ agents = mesh_registry.find_by_capability(to.to_sym)
71
+ return nil if agents.empty?
72
+
73
+ online = agents.select { |a| a[:status] == :online }
74
+ (online.empty? ? agents : online).sample[:agent_id]
75
+ end
76
+
77
+ def task_pending
78
+ @task_pending ||= Helpers::PendingRequests.new(default_ttl: DEFAULT_TIMEOUT)
79
+ end
80
+
81
+ def delegation_tracker
82
+ @delegation_tracker ||= Helpers::Delegation.new
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Mesh
6
- VERSION = '0.2.5'
6
+ VERSION = '0.3.1'
7
7
  end
8
8
  end
9
9
  end
@@ -10,6 +10,7 @@ require 'legion/extensions/mesh/helpers/peer_verify'
10
10
  require 'legion/extensions/mesh/runners/mesh'
11
11
  require 'legion/extensions/mesh/runners/preferences'
12
12
  require 'legion/extensions/mesh/runners/delegation'
13
+ require 'legion/extensions/mesh/runners/task_request'
13
14
 
14
15
  if defined?(Legion::Transport)
15
16
  require 'legion/extensions/mesh/transport/messages/preference_query'
@@ -51,4 +51,26 @@ RSpec.describe Legion::Extensions::Mesh::Client do
51
51
  expect(result[:expired]).to eq(0)
52
52
  end
53
53
  end
54
+
55
+ it 'responds to delegation runner methods' do
56
+ expect(client).to respond_to(:delegate)
57
+ expect(client).to respond_to(:complete_delegation)
58
+ expect(client).to respond_to(:revoke_delegation)
59
+ end
60
+
61
+ it 'responds to task request runner methods' do
62
+ expect(client).to respond_to(:request_task)
63
+ expect(client).to respond_to(:handle_task_reply)
64
+ expect(client).to respond_to(:pending_task_stats)
65
+ end
66
+
67
+ describe '#request_task' do
68
+ it 'sends a task request via the client' do
69
+ client.register(agent_id: 'a', capabilities: [:orchestration])
70
+ client.register(agent_id: 'b', capabilities: [:review])
71
+ result = client.request_task(from: 'a', to: 'b', task: 'review', payload: {})
72
+ expect(result[:success]).to be true
73
+ expect(result[:target_agent]).to eq('b')
74
+ end
75
+ end
54
76
  end
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'legion/extensions/mesh/runners/task_request'
5
+
6
+ RSpec.describe Legion::Extensions::Mesh::Runners::TaskRequest do
7
+ subject(:runner) { Object.new.extend(described_class) }
8
+
9
+ before do
10
+ runner.instance_variable_set(:@mesh_registry, nil)
11
+ runner.instance_variable_set(:@task_pending, nil)
12
+ runner.instance_variable_set(:@delegation_tracker, nil)
13
+ end
14
+
15
+ describe '#request_task' do
16
+ before do
17
+ runner.register(agent_id: 'requester', capabilities: [:orchestration])
18
+ runner.register(agent_id: 'worker-1', capabilities: [:code_review])
19
+ runner.register(agent_id: 'worker-2', capabilities: %i[code_review testing])
20
+ end
21
+
22
+ it 'sends a task request to a specific agent by ID' do
23
+ result = runner.request_task(from: 'requester', to: 'worker-1',
24
+ task: 'review_code', payload: { file: 'app.rb' })
25
+ expect(result[:success]).to be true
26
+ expect(result[:correlation_id]).to be_a(String)
27
+ expect(result[:delegation_id]).to start_with('del-')
28
+ expect(result[:target_agent]).to eq('worker-1')
29
+ end
30
+
31
+ it 'routes to an agent by capability when to: is not a registered agent' do
32
+ result = runner.request_task(from: 'requester', to: 'code_review',
33
+ task: 'review_code', payload: {})
34
+ expect(result[:success]).to be true
35
+ expect(%w[worker-1 worker-2]).to include(result[:target_agent])
36
+ end
37
+
38
+ it 'returns error when no agent found for capability' do
39
+ result = runner.request_task(from: 'requester', to: 'nonexistent_capability',
40
+ task: 'do_thing', payload: {})
41
+ expect(result[:success]).to be false
42
+ expect(result[:reason]).to eq(:no_agent_found)
43
+ end
44
+
45
+ it 'registers a pending request' do
46
+ result = runner.request_task(from: 'requester', to: 'worker-1',
47
+ task: 'review', payload: {}, timeout: 60)
48
+ expect(runner.send(:task_pending).pending?(result[:correlation_id])).to be true
49
+ end
50
+ end
51
+
52
+ describe '#handle_task_reply' do
53
+ it 'resolves a pending request' do
54
+ runner.register(agent_id: 'req', capabilities: [])
55
+ runner.register(agent_id: 'wrk', capabilities: [:work])
56
+
57
+ req = runner.request_task(from: 'req', to: 'wrk', task: 'test', payload: {})
58
+ result = runner.handle_task_reply(
59
+ correlation_id: req[:correlation_id],
60
+ result: { status: :completed, output: 'done' }
61
+ )
62
+ expect(result[:success]).to be true
63
+ expect(result[:resolved]).to be true
64
+ end
65
+
66
+ it 'returns not_found for unknown correlation_id' do
67
+ result = runner.handle_task_reply(correlation_id: 'unknown', result: {})
68
+ expect(result[:success]).to be false
69
+ expect(result[:reason]).to eq(:not_found)
70
+ end
71
+ end
72
+
73
+ describe '#pending_task_stats' do
74
+ it 'returns count of pending requests' do
75
+ runner.register(agent_id: 'a', capabilities: [])
76
+ runner.register(agent_id: 'b', capabilities: [:work])
77
+ runner.request_task(from: 'a', to: 'b', task: 't', payload: {})
78
+ stats = runner.pending_task_stats
79
+ expect(stats[:success]).to be true
80
+ expect(stats[:pending_count]).to be >= 1
81
+ end
82
+ end
83
+
84
+ describe '#expire_pending_tasks' do
85
+ it 'expires timed-out requests' do
86
+ result = runner.expire_pending_tasks
87
+ expect(result[:success]).to be true
88
+ expect(result[:expired_count]).to eq(0)
89
+ end
90
+ end
91
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,16 +1,43 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'bundler/setup'
4
+ require 'legion/logging'
5
+ require 'legion/settings'
6
+ require 'legion/cache/helper'
7
+ require 'legion/crypt/helper'
8
+ require 'legion/data/helper'
9
+ require 'legion/json/helper'
10
+ require 'legion/transport'
4
11
 
5
12
  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
13
+ module Extensions
14
+ module Helpers
15
+ module Lex
16
+ include Legion::Logging::Helper
17
+ include Legion::Settings::Helper
18
+ include Legion::Cache::Helper
19
+ include Legion::Crypt::Helper
20
+ include Legion::Data::Helper
21
+ include Legion::JSON::Helper
22
+ include Legion::Transport::Helper
23
+ end
24
+ end
25
+
26
+ module Actors
27
+ class Every
28
+ include Helpers::Lex
29
+ end
30
+
31
+ class Once
32
+ include Helpers::Lex
33
+ end
34
+ end
11
35
  end
12
36
  end
13
37
 
38
+ Legion::Settings.loader.settings[:client] ||= {}
39
+ Legion::Settings.loader.settings[:client][:name] = 'test-agent'
40
+
14
41
  require 'legion/extensions/mesh'
15
42
 
16
43
  RSpec.configure do |config|
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-mesh
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.5
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -37,6 +37,104 @@ dependencies:
37
37
  - - "~>"
38
38
  - !ruby/object:Gem::Version
39
39
  version: '1.3'
40
+ - !ruby/object:Gem::Dependency
41
+ name: legion-cache
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: 1.3.11
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: 1.3.11
54
+ - !ruby/object:Gem::Dependency
55
+ name: legion-crypt
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: 1.4.9
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: 1.4.9
68
+ - !ruby/object:Gem::Dependency
69
+ name: legion-data
70
+ requirement: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: 1.4.17
75
+ type: :runtime
76
+ prerelease: false
77
+ version_requirements: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: 1.4.17
82
+ - !ruby/object:Gem::Dependency
83
+ name: legion-json
84
+ requirement: !ruby/object:Gem::Requirement
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ version: 1.2.1
89
+ type: :runtime
90
+ prerelease: false
91
+ version_requirements: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - ">="
94
+ - !ruby/object:Gem::Version
95
+ version: 1.2.1
96
+ - !ruby/object:Gem::Dependency
97
+ name: legion-logging
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - ">="
101
+ - !ruby/object:Gem::Version
102
+ version: 1.3.2
103
+ type: :runtime
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: 1.3.2
110
+ - !ruby/object:Gem::Dependency
111
+ name: legion-settings
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: 1.3.14
117
+ type: :runtime
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - ">="
122
+ - !ruby/object:Gem::Version
123
+ version: 1.3.14
124
+ - !ruby/object:Gem::Dependency
125
+ name: legion-transport
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: 1.3.9
131
+ type: :runtime
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: 1.3.9
40
138
  description: Agent-to-agent mesh communication protocol for brain-modeled agentic
41
139
  AI
42
140
  email:
@@ -63,6 +161,7 @@ files:
63
161
  - lib/legion/extensions/mesh/runners/delegation.rb
64
162
  - lib/legion/extensions/mesh/runners/mesh.rb
65
163
  - lib/legion/extensions/mesh/runners/preferences.rb
164
+ - lib/legion/extensions/mesh/runners/task_request.rb
66
165
  - lib/legion/extensions/mesh/transport/messages/gossip.rb
67
166
  - lib/legion/extensions/mesh/transport/messages/mesh_departure.rb
68
167
  - lib/legion/extensions/mesh/transport/messages/preference_query.rb
@@ -86,6 +185,7 @@ files:
86
185
  - spec/legion/extensions/mesh/runners/mesh_gossip_spec.rb
87
186
  - spec/legion/extensions/mesh/runners/mesh_spec.rb
88
187
  - spec/legion/extensions/mesh/runners/preferences_spec.rb
188
+ - spec/legion/extensions/mesh/runners/task_request_spec.rb
89
189
  - spec/legion/extensions/mesh/transport/messages/gossip_spec.rb
90
190
  - spec/legion/extensions/mesh/transport/messages/mesh_departure_spec.rb
91
191
  - spec/legion/extensions/mesh/transport/messages/preference_query_spec.rb