lex-swarm 0.1.2 → 0.2.0

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: 57a1719c0d8b1b40310d6d060c85d6a1e3f7ed54cd35b6de22eae1a63bd79361
4
- data.tar.gz: ed02e732a76958b725dc538bd66e9a1c1605f3a5343f725262e61415e6def46e
3
+ metadata.gz: 578c4f78e11c1cf390624da98867d76b55274db01186715405a0e423645b99ae
4
+ data.tar.gz: 4584ad7f4ebe1c53eef32337404d21f930e23703f182f5f2ea835bb2c6982778
5
5
  SHA512:
6
- metadata.gz: 98f84a0186ecfcaa0ba734e506f5d71f604759012aec2104490d3d5b2dd74f9ebe28e1c987ea8a4752e298a581f6584990ce830e1c575696f68c783a265fdd31
7
- data.tar.gz: f1edc98f2e926bb94998fd4cce549d4d817d44b2a52e7466552324298ef186e8b1366cd27f4eb5bf0c8344ce9c9de0dca7091d4c25fb4df13a0a178ac964ed8e
6
+ metadata.gz: be53ef935c5ae6c6196ed41ffa15b0cbfc8ce1c708d37fccf2ecc8d75c91a511f6e2132952a7449fa58e9d56d762a8a72876a0cb9c81a0b93ce214399b168f9b
7
+ data.tar.gz: 5707c99712feea161e00e88a70767e74bbdd3daddb9509d99cd4b94d5236a9f246cedf109cba9a64d1077311f540163418f1f81b47f0603583334661285ad4dc
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'time'
4
+ require_relative '../helpers/workspace'
5
+
6
+ module Legion
7
+ module Extensions
8
+ module Swarm
9
+ module Actors
10
+ class WorkspaceSync
11
+ ROUTING_PREFIX = 'swarm.workspace'
12
+
13
+ def publish_change(charter_id:, key:, operation:, value: nil, author: nil, version: nil, **) # rubocop:disable Metrics/ParameterLists
14
+ return { success: true, skipped: :no_transport } unless defined?(Legion::Transport)
15
+
16
+ routing_key = "#{ROUTING_PREFIX}.#{charter_id}"
17
+ payload = { charter_id: charter_id, key: key, value: value,
18
+ author: author, version: version, operation: operation,
19
+ timestamp: Time.now.utc.to_s }
20
+
21
+ Legion::Transport.publish(routing_key: routing_key, payload: payload)
22
+ Legion::Logging.debug "[swarm-workspace-sync] published #{operation} #{key} to #{routing_key}"
23
+ { success: true, routing_key: routing_key }
24
+ rescue StandardError => e
25
+ Legion::Logging.warn "[swarm-workspace-sync] publish failed: #{e.message}"
26
+ { success: true, skipped: :publish_error, message: e.message }
27
+ end
28
+
29
+ def apply_incoming(charter_id:, key:, operation:, value: nil, author: nil, version: nil, timestamp: nil, **) # rubocop:disable Metrics/ParameterLists
30
+ op = operation.to_s
31
+ case op
32
+ when 'put'
33
+ ts = timestamp.is_a?(String) ? Time.parse(timestamp) : (timestamp || Time.now.utc)
34
+ applied = workspace.apply_remote(charter_id, key: key, value: value,
35
+ author: author, version: version || 1, timestamp: ts)
36
+ { success: true, applied: applied }
37
+ when 'delete'
38
+ workspace.delete(charter_id, key: key)
39
+ { success: true, applied: true }
40
+ else
41
+ { success: false, reason: :unknown_operation, operation: op }
42
+ end
43
+ end
44
+
45
+ private
46
+
47
+ def workspace
48
+ @workspace ||= Helpers::Workspace.new
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -3,12 +3,14 @@
3
3
  require 'legion/extensions/swarm/helpers/charter'
4
4
  require 'legion/extensions/swarm/helpers/swarm_store'
5
5
  require 'legion/extensions/swarm/runners/swarm'
6
+ require 'legion/extensions/swarm/runners/workspace'
6
7
 
7
8
  module Legion
8
9
  module Extensions
9
10
  module Swarm
10
11
  class Client
11
12
  include Runners::Swarm
13
+ include Runners::Workspace
12
14
 
13
15
  def initialize(**)
14
16
  @swarm_store = Helpers::SwarmStore.new
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Swarm
6
+ module Helpers
7
+ class Workspace
8
+ def initialize
9
+ @store = {}
10
+ @mutex = Mutex.new
11
+ end
12
+
13
+ def put(charter_id, key:, value:, author:)
14
+ @mutex.synchronize do
15
+ charter_store = (@store[charter_id] ||= {})
16
+ existing = charter_store[key]
17
+ version = existing ? existing[:version] + 1 : 1
18
+ charter_store[key] = {
19
+ key: key,
20
+ value: value,
21
+ author: author,
22
+ version: version,
23
+ timestamp: Time.now.utc
24
+ }
25
+ end
26
+ end
27
+
28
+ def get(charter_id, key:)
29
+ @mutex.synchronize do
30
+ @store.dig(charter_id, key)
31
+ end
32
+ end
33
+
34
+ def list(charter_id)
35
+ @mutex.synchronize do
36
+ (@store[charter_id] || {}).dup
37
+ end
38
+ end
39
+
40
+ def delete(charter_id, key:)
41
+ @mutex.synchronize do
42
+ charter_store = @store[charter_id]
43
+ return nil unless charter_store
44
+
45
+ charter_store.delete(key)
46
+ end
47
+ end
48
+
49
+ def clear_charter(charter_id)
50
+ @mutex.synchronize do
51
+ @store.delete(charter_id)
52
+ end
53
+ end
54
+
55
+ def apply_remote(charter_id, **entry)
56
+ key = entry[:key]
57
+ value = entry[:value]
58
+ author = entry[:author]
59
+ version = entry[:version]
60
+ timestamp = entry[:timestamp]
61
+
62
+ @mutex.synchronize do
63
+ charter_store = (@store[charter_id] ||= {})
64
+ existing = charter_store[key]
65
+
66
+ if existing.nil? || version > existing[:version] ||
67
+ (version == existing[:version] && timestamp > existing[:timestamp])
68
+ charter_store[key] = {
69
+ key: key,
70
+ value: value,
71
+ author: author,
72
+ version: version,
73
+ timestamp: timestamp
74
+ }
75
+ return true
76
+ end
77
+ false
78
+ end
79
+ end
80
+
81
+ def stats
82
+ @mutex.synchronize do
83
+ total = @store.values.sum(&:size)
84
+ { charter_count: @store.size, total_entries: total }
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../helpers/workspace'
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Swarm
8
+ module Runners
9
+ module Workspace
10
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
11
+ Legion::Extensions::Helpers.const_defined?(:Lex)
12
+
13
+ def workspace_put(charter_id:, key:, value:, author:, **)
14
+ entry = workspace.put(charter_id, key: key, value: value, author: author)
15
+ Legion::Logging.debug "[swarm-workspace] put: charter=#{charter_id[0..7]} key=#{key} v=#{entry[:version]}"
16
+ { success: true, key: key, version: entry[:version], charter_id: charter_id }
17
+ end
18
+
19
+ def workspace_get(charter_id:, key:, **)
20
+ entry = workspace.get(charter_id, key: key)
21
+ if entry
22
+ { success: true, entry: entry }
23
+ else
24
+ { success: false, reason: :not_found, key: key, charter_id: charter_id }
25
+ end
26
+ end
27
+
28
+ def workspace_list(charter_id:, **)
29
+ entries = workspace.list(charter_id)
30
+ { success: true, entries: entries, count: entries.size }
31
+ end
32
+
33
+ def workspace_delete(charter_id:, key:, **)
34
+ removed = workspace.delete(charter_id, key: key)
35
+ if removed
36
+ Legion::Logging.debug "[swarm-workspace] delete: charter=#{charter_id[0..7]} key=#{key}"
37
+ { success: true, key: key, charter_id: charter_id }
38
+ else
39
+ { success: false, reason: :not_found, key: key, charter_id: charter_id }
40
+ end
41
+ end
42
+
43
+ def workspace_clear(charter_id:, **)
44
+ workspace.clear_charter(charter_id)
45
+ Legion::Logging.debug "[swarm-workspace] clear: charter=#{charter_id[0..7]}"
46
+ { success: true, charter_id: charter_id }
47
+ end
48
+
49
+ def workspace_stats(**)
50
+ workspace.stats.merge(success: true)
51
+ end
52
+
53
+ private
54
+
55
+ def workspace
56
+ @workspace ||= Helpers::Workspace.new
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Swarm
6
- VERSION = '0.1.2'
6
+ VERSION = '0.2.0'
7
7
  end
8
8
  end
9
9
  end
@@ -4,8 +4,10 @@ require 'legion/extensions/swarm/version'
4
4
  require 'legion/extensions/swarm/helpers/charter'
5
5
  require 'legion/extensions/swarm/helpers/swarm_store'
6
6
  require 'legion/extensions/swarm/helpers/sub_agent'
7
+ require 'legion/extensions/swarm/helpers/workspace'
7
8
  require 'legion/extensions/swarm/runners/swarm'
8
9
  require 'legion/extensions/swarm/runners/spawn_child'
10
+ require 'legion/extensions/swarm/runners/workspace'
9
11
 
10
12
  module Legion
11
13
  module Extensions
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require_relative '../../../../../lib/legion/extensions/swarm/actors/workspace_sync'
5
+
6
+ RSpec.describe Legion::Extensions::Swarm::Actors::WorkspaceSync do
7
+ subject(:actor) { described_class.allocate }
8
+
9
+ describe '#publish_change' do
10
+ it 'returns skipped when transport is not available' do
11
+ result = actor.publish_change(charter_id: 'c1', key: 'k', value: 'v',
12
+ author: 'a', version: 1, operation: :put)
13
+ expect(result[:success]).to be true
14
+ expect(result[:skipped]).to eq(:no_transport)
15
+ end
16
+ end
17
+
18
+ describe '#apply_incoming' do
19
+ let(:workspace) { Legion::Extensions::Swarm::Helpers::Workspace.new }
20
+
21
+ before { actor.instance_variable_set(:@workspace, workspace) }
22
+
23
+ it 'applies a remote put operation' do
24
+ result = actor.apply_incoming(
25
+ charter_id: 'c1', key: 'shared', value: 'remote-data',
26
+ author: 'remote-agent', version: 1, timestamp: Time.now.utc.to_s, operation: 'put'
27
+ )
28
+ expect(result[:success]).to be true
29
+ expect(result[:applied]).to be true
30
+ expect(workspace.get('c1', key: 'shared')[:value]).to eq('remote-data')
31
+ end
32
+
33
+ it 'applies a remote delete operation' do
34
+ workspace.put('c1', key: 'doomed', value: 'bye', author: 'local')
35
+ result = actor.apply_incoming(
36
+ charter_id: 'c1', key: 'doomed', operation: 'delete'
37
+ )
38
+ expect(result[:success]).to be true
39
+ expect(workspace.get('c1', key: 'doomed')).to be_nil
40
+ end
41
+
42
+ it 'rejects unknown operations' do
43
+ result = actor.apply_incoming(charter_id: 'c1', key: 'x', operation: 'explode')
44
+ expect(result[:success]).to be false
45
+ expect(result[:reason]).to eq(:unknown_operation)
46
+ end
47
+ end
48
+ end
@@ -13,4 +13,24 @@ RSpec.describe Legion::Extensions::Swarm::Client do
13
13
  expect(client).to respond_to(:active_swarms)
14
14
  expect(client).to respond_to(:swarm_status)
15
15
  end
16
+
17
+ it 'responds to workspace runner methods' do
18
+ client = described_class.new
19
+ expect(client).to respond_to(:workspace_put)
20
+ expect(client).to respond_to(:workspace_get)
21
+ expect(client).to respond_to(:workspace_list)
22
+ expect(client).to respond_to(:workspace_delete)
23
+ expect(client).to respond_to(:workspace_clear)
24
+ expect(client).to respond_to(:workspace_stats)
25
+ end
26
+
27
+ describe '#workspace_put and #workspace_get' do
28
+ it 'stores and retrieves workspace entries via client' do
29
+ client = described_class.new
30
+ client.workspace_put(charter_id: 'c1', key: 'data', value: 'hello', author: 'a')
31
+ result = client.workspace_get(charter_id: 'c1', key: 'data')
32
+ expect(result[:success]).to be true
33
+ expect(result[:entry][:value]).to eq('hello')
34
+ end
35
+ end
16
36
  end
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Legion::Extensions::Swarm::Helpers::Workspace do
6
+ subject(:workspace) { described_class.new }
7
+
8
+ let(:charter_id) { 'charter-abc' }
9
+
10
+ describe '#put and #get' do
11
+ it 'stores and retrieves an entry' do
12
+ workspace.put(charter_id, key: 'findings', value: { data: [1, 2, 3] }, author: 'agent-a')
13
+ entry = workspace.get(charter_id, key: 'findings')
14
+ expect(entry[:value]).to eq({ data: [1, 2, 3] })
15
+ expect(entry[:author]).to eq('agent-a')
16
+ expect(entry[:version]).to eq(1)
17
+ end
18
+
19
+ it 'increments version on overwrite' do
20
+ workspace.put(charter_id, key: 'result', value: 'v1', author: 'agent-a')
21
+ workspace.put(charter_id, key: 'result', value: 'v2', author: 'agent-b')
22
+ entry = workspace.get(charter_id, key: 'result')
23
+ expect(entry[:value]).to eq('v2')
24
+ expect(entry[:author]).to eq('agent-b')
25
+ expect(entry[:version]).to eq(2)
26
+ end
27
+
28
+ it 'returns nil for missing key' do
29
+ expect(workspace.get(charter_id, key: 'nope')).to be_nil
30
+ end
31
+
32
+ it 'returns nil for missing charter' do
33
+ expect(workspace.get('nonexistent', key: 'x')).to be_nil
34
+ end
35
+ end
36
+
37
+ describe '#list' do
38
+ it 'returns all entries for a charter' do
39
+ workspace.put(charter_id, key: 'a', value: 1, author: 'x')
40
+ workspace.put(charter_id, key: 'b', value: 2, author: 'y')
41
+ entries = workspace.list(charter_id)
42
+ expect(entries.keys).to contain_exactly('a', 'b')
43
+ end
44
+
45
+ it 'returns empty hash for unknown charter' do
46
+ expect(workspace.list('unknown')).to eq({})
47
+ end
48
+ end
49
+
50
+ describe '#delete' do
51
+ it 'removes an entry and returns it' do
52
+ workspace.put(charter_id, key: 'temp', value: 'data', author: 'a')
53
+ removed = workspace.delete(charter_id, key: 'temp')
54
+ expect(removed[:value]).to eq('data')
55
+ expect(workspace.get(charter_id, key: 'temp')).to be_nil
56
+ end
57
+
58
+ it 'returns nil when key does not exist' do
59
+ expect(workspace.delete(charter_id, key: 'nope')).to be_nil
60
+ end
61
+ end
62
+
63
+ describe '#clear_charter' do
64
+ it 'removes all entries for a charter' do
65
+ workspace.put(charter_id, key: 'a', value: 1, author: 'x')
66
+ workspace.put(charter_id, key: 'b', value: 2, author: 'x')
67
+ workspace.clear_charter(charter_id)
68
+ expect(workspace.list(charter_id)).to eq({})
69
+ end
70
+ end
71
+
72
+ describe '#apply_remote' do
73
+ it 'applies a remote put when version is higher' do
74
+ workspace.put(charter_id, key: 'shared', value: 'local', author: 'a')
75
+ workspace.apply_remote(charter_id, key: 'shared', value: 'remote', author: 'b',
76
+ version: 5, timestamp: Time.now.utc)
77
+ expect(workspace.get(charter_id, key: 'shared')[:value]).to eq('remote')
78
+ expect(workspace.get(charter_id, key: 'shared')[:version]).to eq(5)
79
+ end
80
+
81
+ it 'ignores remote put when version is lower' do
82
+ workspace.put(charter_id, key: 'shared', value: 'local', author: 'a')
83
+ workspace.put(charter_id, key: 'shared', value: 'local2', author: 'a')
84
+ workspace.apply_remote(charter_id, key: 'shared', value: 'stale', author: 'b',
85
+ version: 1, timestamp: Time.now.utc)
86
+ expect(workspace.get(charter_id, key: 'shared')[:value]).to eq('local2')
87
+ end
88
+
89
+ it 'uses timestamp to break version ties' do
90
+ workspace.put(charter_id, key: 'tie', value: 'old', author: 'a')
91
+ later = Time.now.utc + 1
92
+ workspace.apply_remote(charter_id, key: 'tie', value: 'newer', author: 'b',
93
+ version: 1, timestamp: later)
94
+ expect(workspace.get(charter_id, key: 'tie')[:value]).to eq('newer')
95
+ end
96
+ end
97
+
98
+ describe '#stats' do
99
+ it 'returns charter count and total entries' do
100
+ workspace.put('c1', key: 'a', value: 1, author: 'x')
101
+ workspace.put('c1', key: 'b', value: 2, author: 'x')
102
+ workspace.put('c2', key: 'c', value: 3, author: 'y')
103
+ stats = workspace.stats
104
+ expect(stats[:charter_count]).to eq(2)
105
+ expect(stats[:total_entries]).to eq(3)
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe Legion::Extensions::Swarm::Runners::Workspace do
6
+ subject(:runner) { Object.new.extend(described_class) }
7
+
8
+ let(:charter_id) { 'charter-123' }
9
+
10
+ before { runner.instance_variable_set(:@workspace, nil) }
11
+
12
+ describe '#workspace_put' do
13
+ it 'stores a value and returns success' do
14
+ result = runner.workspace_put(charter_id: charter_id, key: 'notes', value: 'hello', author: 'agent-a')
15
+ expect(result[:success]).to be true
16
+ expect(result[:key]).to eq('notes')
17
+ expect(result[:version]).to eq(1)
18
+ end
19
+ end
20
+
21
+ describe '#workspace_get' do
22
+ it 'retrieves a stored value' do
23
+ runner.workspace_put(charter_id: charter_id, key: 'data', value: [1, 2], author: 'a')
24
+ result = runner.workspace_get(charter_id: charter_id, key: 'data')
25
+ expect(result[:success]).to be true
26
+ expect(result[:entry][:value]).to eq([1, 2])
27
+ end
28
+
29
+ it 'returns not_found for missing key' do
30
+ result = runner.workspace_get(charter_id: charter_id, key: 'nope')
31
+ expect(result[:success]).to be false
32
+ expect(result[:reason]).to eq(:not_found)
33
+ end
34
+ end
35
+
36
+ describe '#workspace_list' do
37
+ it 'lists all entries for a charter' do
38
+ runner.workspace_put(charter_id: charter_id, key: 'a', value: 1, author: 'x')
39
+ runner.workspace_put(charter_id: charter_id, key: 'b', value: 2, author: 'x')
40
+ result = runner.workspace_list(charter_id: charter_id)
41
+ expect(result[:success]).to be true
42
+ expect(result[:count]).to eq(2)
43
+ end
44
+ end
45
+
46
+ describe '#workspace_delete' do
47
+ it 'deletes an entry and returns success' do
48
+ runner.workspace_put(charter_id: charter_id, key: 'temp', value: 'x', author: 'a')
49
+ result = runner.workspace_delete(charter_id: charter_id, key: 'temp')
50
+ expect(result[:success]).to be true
51
+ end
52
+
53
+ it 'returns not_found for missing key' do
54
+ result = runner.workspace_delete(charter_id: charter_id, key: 'nope')
55
+ expect(result[:success]).to be false
56
+ expect(result[:reason]).to eq(:not_found)
57
+ end
58
+ end
59
+
60
+ describe '#workspace_clear' do
61
+ it 'clears all entries for a charter' do
62
+ runner.workspace_put(charter_id: charter_id, key: 'a', value: 1, author: 'x')
63
+ result = runner.workspace_clear(charter_id: charter_id)
64
+ expect(result[:success]).to be true
65
+ expect(runner.workspace_list(charter_id: charter_id)[:count]).to eq(0)
66
+ end
67
+ end
68
+
69
+ describe '#workspace_stats' do
70
+ it 'returns workspace statistics' do
71
+ runner.workspace_put(charter_id: 'c1', key: 'a', value: 1, author: 'x')
72
+ runner.workspace_put(charter_id: 'c2', key: 'b', value: 2, author: 'y')
73
+ result = runner.workspace_stats
74
+ expect(result[:success]).to be true
75
+ expect(result[:charter_count]).to eq(2)
76
+ expect(result[:total_entries]).to eq(2)
77
+ end
78
+ end
79
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-swarm
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -35,21 +35,27 @@ files:
35
35
  - lib/legion/extensions/swarm.rb
36
36
  - lib/legion/extensions/swarm/actors/orphan_sweeper.rb
37
37
  - lib/legion/extensions/swarm/actors/stale_check.rb
38
+ - lib/legion/extensions/swarm/actors/workspace_sync.rb
38
39
  - lib/legion/extensions/swarm/client.rb
39
40
  - lib/legion/extensions/swarm/helpers/charter.rb
40
41
  - lib/legion/extensions/swarm/helpers/sub_agent.rb
41
42
  - lib/legion/extensions/swarm/helpers/swarm_store.rb
43
+ - lib/legion/extensions/swarm/helpers/workspace.rb
42
44
  - lib/legion/extensions/swarm/runners/spawn_child.rb
43
45
  - lib/legion/extensions/swarm/runners/swarm.rb
46
+ - lib/legion/extensions/swarm/runners/workspace.rb
44
47
  - lib/legion/extensions/swarm/version.rb
45
48
  - spec/legion/extensions/swarm/actors/orphan_sweeper_spec.rb
46
49
  - spec/legion/extensions/swarm/actors/stale_check_spec.rb
50
+ - spec/legion/extensions/swarm/actors/workspace_sync_spec.rb
47
51
  - spec/legion/extensions/swarm/client_spec.rb
48
52
  - spec/legion/extensions/swarm/helpers/charter_spec.rb
49
53
  - spec/legion/extensions/swarm/helpers/sub_agent_spec.rb
50
54
  - spec/legion/extensions/swarm/helpers/swarm_store_spec.rb
55
+ - spec/legion/extensions/swarm/helpers/workspace_spec.rb
51
56
  - spec/legion/extensions/swarm/runners/spawn_child_spec.rb
52
57
  - spec/legion/extensions/swarm/runners/swarm_spec.rb
58
+ - spec/legion/extensions/swarm/runners/workspace_spec.rb
53
59
  - spec/spec_helper.rb
54
60
  homepage: https://github.com/LegionIO/lex-swarm
55
61
  licenses: