legionio 1.6.21 → 1.6.24

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.
data/lib/legion/api.rb CHANGED
@@ -47,6 +47,10 @@ require_relative 'api/costs'
47
47
  require_relative 'api/traces'
48
48
  require_relative 'api/stats'
49
49
  require_relative 'api/codegen'
50
+ require_relative 'api/router'
51
+ require_relative 'api/library_routes'
52
+ require_relative 'api/sync_dispatch'
53
+ require_relative 'api/lex_dispatch'
50
54
  require_relative 'api/graphql' if defined?(GraphQL)
51
55
 
52
56
  module Legion
@@ -72,6 +76,21 @@ module Legion
72
76
  Legion::API::OpenAPI.to_json
73
77
  end
74
78
 
79
+ # Root discovery — lists all tiers
80
+ get '/api/discovery' do
81
+ content_type :json
82
+ Legion::JSON.dump({
83
+ infrastructure: [
84
+ { path: '/api/health', method: 'GET' },
85
+ { path: '/api/ready', method: 'GET' },
86
+ { path: '/api/openapi.json', method: 'GET' },
87
+ { path: '/api/discovery', method: 'GET' }
88
+ ],
89
+ libraries: Legion::API.router.library_names,
90
+ extensions: Legion::API.router.extension_names
91
+ })
92
+ end
93
+
75
94
  # Health and readiness
76
95
  get '/api/health' do
77
96
  json_response({ status: 'ok', version: Legion::VERSION })
@@ -87,8 +106,14 @@ module Legion
87
106
  content_type :json
88
107
  Legion::Logging.warn "API #{request.request_method} #{request.path_info} returned 404: no route matches"
89
108
  Legion::JSON.dump({
90
- error: { code: 'not_found', message: "no route matches #{request.request_method} #{request.path_info}" },
91
- meta: { timestamp: Time.now.utc.iso8601, node: Legion::Settings[:client][:name] }
109
+ task_id: nil,
110
+ conversation_id: nil,
111
+ status: 'failed',
112
+ error: {
113
+ code: 404,
114
+ message: "no route matches #{request.request_method} #{request.path_info}"
115
+ },
116
+ meta: { timestamp: Time.now.utc.iso8601, node: Legion::Settings[:client][:name] }
92
117
  })
93
118
  end
94
119
 
@@ -97,12 +122,16 @@ module Legion
97
122
  err = env['sinatra.error']
98
123
  Legion::Logging.log_exception(err, payload_summary: "API #{request.request_method} #{request.path_info} returned 500", component_type: :api)
99
124
  Legion::JSON.dump({
100
- error: { code: 'internal_error', message: err.message },
101
- meta: { timestamp: Time.now.utc.iso8601, node: Legion::Settings[:client][:name] }
125
+ task_id: nil,
126
+ conversation_id: nil,
127
+ status: 'failed',
128
+ error: { code: 500, message: err.message },
129
+ meta: { timestamp: Time.now.utc.iso8601, node: Legion::Settings[:client][:name] }
102
130
  })
103
131
  end
104
132
 
105
133
  # Mount route modules
134
+ register Routes::LexDispatch
106
135
  register Routes::Tasks
107
136
  register Routes::Extensions
108
137
  register Routes::Nodes
@@ -126,14 +155,14 @@ module Legion
126
155
  register Routes::Capacity
127
156
  register Routes::Audit
128
157
  register Routes::Metrics
129
- register Routes::Llm
158
+ register Routes::Llm unless defined?(Legion::LLM::Routes)
130
159
  register Routes::ExtensionCatalog
131
160
  register Routes::OrgChart
132
161
  register Routes::Governance
133
162
  register Routes::Acp
134
163
  register Routes::Prompts
135
164
  register Routes::Marketplace
136
- register Routes::Apollo
165
+ register Routes::Apollo unless defined?(Legion::Apollo::Routes)
137
166
  register Routes::Costs
138
167
  register Routes::Traces
139
168
  register Routes::Stats
@@ -143,6 +172,13 @@ module Legion
143
172
  use Legion::API::Middleware::RequestLogger
144
173
  use Legion::Rbac::Middleware if defined?(Legion::Rbac::Middleware)
145
174
 
175
+ # Tier-aware router (three-tier namespace)
176
+ class << self
177
+ def router
178
+ @router ||= Legion::API::Router.new
179
+ end
180
+ end
181
+
146
182
  # Hook registry (preserved from original implementation)
147
183
  class << self
148
184
  def hook_registry
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'net/http'
5
+ require 'erb'
6
+
7
+ module Legion
8
+ module CLI
9
+ module Admin
10
+ class PurgeTopology < Thor
11
+ namespace 'admin:purge_topology'
12
+
13
+ def self.exit_on_failure?
14
+ true
15
+ end
16
+
17
+ class_option :json, type: :boolean, default: false, desc: 'Output as JSON'
18
+ class_option :no_color, type: :boolean, default: false, desc: 'Disable color output'
19
+ class_option :host, type: :string, default: 'localhost', desc: 'RabbitMQ management host'
20
+ class_option :port, type: :numeric, default: 15_672, desc: 'RabbitMQ management port'
21
+ class_option :user, type: :string, default: 'guest', desc: 'RabbitMQ management username'
22
+ class_option :password, type: :string, default: 'guest', desc: 'RabbitMQ management password'
23
+ class_option :vhost, type: :string, default: '/', desc: 'RabbitMQ vhost'
24
+ class_option :execute, type: :boolean, default: false, desc: 'Actually delete (default: dry-run)'
25
+
26
+ desc 'purge', 'Enumerate and optionally delete legacy v2.0 topology (legion.{lex} exchanges/queues)'
27
+ def purge
28
+ out = formatter
29
+ out.header('Legion AMQP Topology Migration: v2.0 → v3.0')
30
+ out.spacer
31
+
32
+ legacy = find_legacy_topology
33
+ if legacy[:exchanges].empty? && legacy[:queues].empty?
34
+ out.success('No legacy topology found. Already on v3.0 or never had v2.0 topology.')
35
+ return
36
+ end
37
+
38
+ if options[:json]
39
+ perform_deletion(legacy) if options[:execute]
40
+ out.json({ legacy: legacy, deleted: options[:execute] })
41
+ return
42
+ end
43
+
44
+ report_legacy(out, legacy)
45
+
46
+ if options[:execute]
47
+ perform_deletion(legacy)
48
+ out.success("Deleted #{legacy[:exchanges].size} exchange(s) and #{legacy[:queues].size} queue(s)")
49
+ else
50
+ out.warn('Dry-run mode — pass --execute to delete legacy topology')
51
+ end
52
+ rescue Legion::CLI::Error => e
53
+ formatter.error(e.message)
54
+ exit(1)
55
+ end
56
+
57
+ no_commands do # rubocop:disable Metrics/BlockLength
58
+ def formatter
59
+ @formatter ||= Output::Formatter.new(
60
+ json: options[:json],
61
+ color: !options[:no_color]
62
+ )
63
+ end
64
+
65
+ private
66
+
67
+ def vhost_encoded
68
+ ERB::Util.url_encode(options[:vhost])
69
+ end
70
+
71
+ def management_api(path)
72
+ uri = URI("http://#{options[:host]}:#{options[:port]}/api#{path}")
73
+ req = Net::HTTP::Get.new(uri)
74
+ req.basic_auth(options[:user], options[:password])
75
+ response = Net::HTTP.start(uri.host, uri.port, open_timeout: 5, read_timeout: 10) do |http|
76
+ http.request(req)
77
+ end
78
+ raise Legion::CLI::Error, "Management API #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
79
+
80
+ ::JSON.parse(response.body, symbolize_names: true)
81
+ rescue Errno::ECONNREFUSED
82
+ raise Legion::CLI::Error, "Cannot connect to RabbitMQ management API at #{options[:host]}:#{options[:port]}"
83
+ rescue Net::OpenTimeout, Net::ReadTimeout
84
+ raise Legion::CLI::Error, 'Timed out connecting to RabbitMQ management API'
85
+ end
86
+
87
+ def management_delete(path)
88
+ uri = URI("http://#{options[:host]}:#{options[:port]}/api#{path}")
89
+ req = Net::HTTP::Delete.new(uri)
90
+ req.basic_auth(options[:user], options[:password])
91
+ response = Net::HTTP.start(uri.host, uri.port, open_timeout: 5, read_timeout: 10) do |http|
92
+ http.request(req)
93
+ end
94
+ raise Legion::CLI::Error, "Management API #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
95
+
96
+ response
97
+ rescue Errno::ECONNREFUSED
98
+ raise Legion::CLI::Error, "Cannot connect to RabbitMQ management API at #{options[:host]}:#{options[:port]}"
99
+ rescue Net::OpenTimeout, Net::ReadTimeout
100
+ raise Legion::CLI::Error, 'Timed out connecting to RabbitMQ management API'
101
+ end
102
+
103
+ # Find exchanges and queues matching legacy v2.0 pattern: legion.{lex_name}.*
104
+ # but NOT matching v3.0 pattern (lex.{lex_name}.*) or infrastructure (task, node, etc.)
105
+ def find_legacy_topology
106
+ all_exchanges = management_api("/exchanges/#{vhost_encoded}")
107
+ all_queues = management_api("/queues/#{vhost_encoded}")
108
+
109
+ legacy_exchanges = all_exchanges
110
+ .map { |e| e[:name].to_s }
111
+ .select do |name|
112
+ name.match?(/\Alegion\.[a-z]/) && !name.start_with?('legion.task', 'legion.node', 'legion.crypt', 'legion.extensions',
113
+ 'legion.logging')
114
+ end
115
+
116
+ legacy_queues = all_queues
117
+ .map { |q| q[:name].to_s }
118
+ .select { |name| name.match?(/\Alegion\.[a-z]/) && !name.match?(/\Alegion\.(task|node|crypt|extensions|logging)/) }
119
+
120
+ { exchanges: legacy_exchanges, queues: legacy_queues }
121
+ end
122
+
123
+ def report_legacy(out, legacy)
124
+ unless legacy[:exchanges].empty?
125
+ out.detail_header("Legacy Exchanges (#{legacy[:exchanges].size})")
126
+ legacy[:exchanges].each { |name| out.detail({ name: name }) }
127
+ out.spacer
128
+ end
129
+
130
+ unless legacy[:queues].empty? # rubocop:disable Style/GuardClause
131
+ out.detail_header("Legacy Queues (#{legacy[:queues].size})")
132
+ legacy[:queues].each { |name| out.detail({ name: name }) }
133
+ out.spacer
134
+ end
135
+ end
136
+
137
+ def perform_deletion(legacy)
138
+ legacy[:queues].each do |name|
139
+ management_delete("/queues/#{vhost_encoded}/#{ERB::Util.url_encode(name)}")
140
+ end
141
+ legacy[:exchanges].each do |name|
142
+ management_delete("/exchanges/#{vhost_encoded}/#{ERB::Util.url_encode(name)}")
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/cli/chat_command'
4
+
5
+ begin
6
+ require 'legion/llm/daemon_client'
7
+ rescue LoadError
8
+ # legion-llm not yet loaded; DaemonClient must be defined before DaemonChat#ask is called.
9
+ end
10
+
11
+ module Legion
12
+ module CLI
13
+ class Chat
14
+ # Daemon-backed chat adapter. Matches the interface that Session expects
15
+ # from a chat object (ask, with_tools, with_instructions, on_tool_call,
16
+ # on_tool_result, model, add_message, reset_messages!, with_model).
17
+ #
18
+ # All LLM inference is routed through the running daemon via
19
+ # POST /api/llm/inference. Tool execution runs locally on the client
20
+ # machine — the daemon returns tool_call requests and the client
21
+ # executes them and loops.
22
+ class DaemonChat
23
+ # Minimal response-like object returned from ask.
24
+ # Responds to the same interface Session#send_message reads.
25
+ Response = Struct.new(:content, :input_tokens, :output_tokens, :model)
26
+
27
+ # Minimal model object responding to .id (used by Session#model_id).
28
+ ModelInfo = Struct.new(:id) do
29
+ def to_s
30
+ id.to_s
31
+ end
32
+ end
33
+
34
+ attr_reader :model
35
+
36
+ def initialize(model: nil, provider: nil)
37
+ @model = ModelInfo.new(id: model)
38
+ @provider = provider
39
+ @messages = []
40
+ @tools = []
41
+ @instructions = nil
42
+ @on_tool_call = nil
43
+ @on_tool_result = nil
44
+ end
45
+
46
+ # Sets the system prompt. Returns self for chaining.
47
+ def with_instructions(prompt)
48
+ @instructions = prompt
49
+ self
50
+ end
51
+
52
+ # Registers tool classes for local execution and schema forwarding.
53
+ # Returns self for chaining.
54
+ def with_tools(*tools)
55
+ @tools = tools.flatten
56
+ self
57
+ end
58
+
59
+ # Switches the active model. Returns self for chaining.
60
+ def with_model(model_id)
61
+ @model = ModelInfo.new(id: model_id)
62
+ self
63
+ end
64
+
65
+ # Stores a tool_call callback invoked before each local tool execution.
66
+ def on_tool_call(&block)
67
+ @on_tool_call = block
68
+ end
69
+
70
+ # Stores a tool_result callback invoked after each local tool execution.
71
+ def on_tool_result(&block)
72
+ @on_tool_result = block
73
+ end
74
+
75
+ # Appends a message to the conversation history directly (used by
76
+ # slash commands /fetch, /search, /agent, etc. that inject context).
77
+ def add_message(role:, content:)
78
+ @messages << { role: role.to_s, content: content }
79
+ end
80
+
81
+ # Clears all conversation history (used by /clear slash command).
82
+ def reset_messages!
83
+ @messages = []
84
+ end
85
+
86
+ # Sends a message through the daemon inference loop.
87
+ # Executes any tool_calls locally and loops until the LLM stops.
88
+ # Yields response-like chunks for streaming display (Phase 1: single chunk).
89
+ # Returns a Response object compatible with Session#send_message.
90
+ def ask(message, &on_chunk)
91
+ @messages << { role: 'user', content: message }
92
+
93
+ loop do
94
+ result = call_daemon_inference
95
+
96
+ raise CLI::Error, "Daemon inference error: #{result[:error]}" if result[:status] == :error
97
+ raise CLI::Error, 'Daemon is unavailable' if result[:status] == :unavailable
98
+
99
+ data = extract_data(result)
100
+
101
+ if data[:tool_calls]&.any?
102
+ execute_tool_calls(data[:tool_calls], data[:content])
103
+ else
104
+ on_chunk&.call(Response.new(content: data[:content]))
105
+ @messages << { role: 'assistant', content: data[:content] }
106
+ return build_response(data)
107
+ end
108
+ end
109
+ end
110
+
111
+ private
112
+
113
+ def call_daemon_inference
114
+ Legion::LLM::DaemonClient.inference(
115
+ messages: build_messages,
116
+ tools: build_tool_schemas,
117
+ model: @model.id,
118
+ provider: @provider
119
+ )
120
+ end
121
+
122
+ def extract_data(result)
123
+ # DaemonClient.inference returns { status:, data: { content:, tool_calls:, ... } }
124
+ data = result[:data] || result[:body] || {}
125
+ data.is_a?(Hash) ? data : {}
126
+ end
127
+
128
+ def build_messages
129
+ msgs = []
130
+ msgs << { role: 'system', content: @instructions } if @instructions
131
+ msgs + @messages
132
+ end
133
+
134
+ def build_tool_schemas
135
+ @tools.map do |tool|
136
+ {
137
+ name: tool_name(tool),
138
+ description: tool_description(tool),
139
+ parameters: tool_parameters(tool)
140
+ }
141
+ end
142
+ end
143
+
144
+ def tool_name(tool)
145
+ if tool.respond_to?(:tool_name)
146
+ tool.tool_name
147
+ else
148
+ tool.name.to_s.split('::').last.gsub(/([A-Z])/) do
149
+ "_#{::Regexp.last_match(1).downcase}"
150
+ end.delete_prefix('_')
151
+ end
152
+ end
153
+
154
+ def tool_description(tool)
155
+ tool.respond_to?(:description) ? tool.description : ''
156
+ end
157
+
158
+ def tool_parameters(tool)
159
+ tool.respond_to?(:parameters) ? tool.parameters : {}
160
+ end
161
+
162
+ def execute_tool_calls(tool_calls, assistant_content)
163
+ # Record the assistant turn with tool_calls before appending results.
164
+ @messages << { role: 'assistant', content: assistant_content, tool_calls: tool_calls }
165
+
166
+ tool_calls.each do |tc|
167
+ tc = tc.transform_keys(&:to_sym) if tc.respond_to?(:transform_keys)
168
+ tc_obj = build_tool_call_object(tc)
169
+
170
+ @on_tool_call&.call(tc_obj)
171
+
172
+ result_text = run_tool(tc)
173
+
174
+ result_obj = build_tool_result_object(result_text)
175
+ @on_tool_result&.call(result_obj)
176
+
177
+ @messages << {
178
+ role: 'tool',
179
+ tool_call_id: tc[:id] || tc[:tool_call_id],
180
+ content: result_text.to_s
181
+ }
182
+ end
183
+ end
184
+
185
+ def build_tool_call_object(tool_call)
186
+ Struct.new(:name, :arguments, :id).new(
187
+ name: tool_call[:name].to_s,
188
+ arguments: (tool_call[:arguments] || tool_call[:input] || {}).transform_keys(&:to_sym),
189
+ id: tool_call[:id] || tool_call[:tool_call_id]
190
+ )
191
+ end
192
+
193
+ def build_tool_result_object(text)
194
+ Struct.new(:content).new(content: text.to_s)
195
+ end
196
+
197
+ def run_tool(tool_call)
198
+ name = tool_call[:name].to_s
199
+ arguments = (tool_call[:arguments] || tool_call[:input] || {}).transform_keys(&:to_sym)
200
+
201
+ tool_class = @tools.find { |t| tool_name(t) == name }
202
+ return "Unknown tool: #{name}" unless tool_class
203
+
204
+ tool_class.call(**arguments)
205
+ rescue StandardError => e
206
+ "Tool error (#{name}): #{e.message}"
207
+ end
208
+
209
+ def build_response(data)
210
+ Response.new(
211
+ content: data[:content],
212
+ input_tokens: data[:input_tokens],
213
+ output_tokens: data[:output_tokens],
214
+ model: ModelInfo.new(id: data[:model] || @model.id)
215
+ )
216
+ end
217
+ end
218
+ end
219
+ end
220
+ end
@@ -176,7 +176,14 @@ module Legion
176
176
  def setup_connection
177
177
  Connection.config_dir = options[:config_dir] if options[:config_dir]
178
178
  Connection.log_level = options[:verbose] ? 'debug' : 'error'
179
- Connection.ensure_llm
179
+ Connection.ensure_settings
180
+
181
+ require 'legion/llm/daemon_client'
182
+ return if Legion::LLM::DaemonClient.available?
183
+
184
+ raise CLI::Error,
185
+ "LegionIO daemon is not running. Start it with: legionio start\n " \
186
+ 'All LLM requests must route through the daemon.'
180
187
  end
181
188
 
182
189
  def setup_notification_bridge
@@ -237,13 +244,13 @@ module Legion
237
244
  end
238
245
 
239
246
  def create_chat
240
- opts = {}
241
- opts[:model] = options[:model] || chat_setting(:model)
242
- opts[:provider] = (options[:provider] || chat_setting(:provider))&.to_sym
243
- opts.compact!
244
-
247
+ require 'legion/cli/chat/daemon_chat'
245
248
  require 'legion/cli/chat/tool_registry'
246
- chat = Legion::LLM.chat(**opts, caller: { source: 'cli', command: 'chat' })
249
+
250
+ chat = Chat::DaemonChat.new(
251
+ model: options[:model] || chat_setting(:model),
252
+ provider: (options[:provider] || chat_setting(:provider))&.to_sym
253
+ )
247
254
  chat.with_tools(*Chat::ToolRegistry.all_tools)
248
255
  chat
249
256
  end
@@ -427,9 +427,9 @@ module Legion
427
427
  end
428
428
  end
429
429
 
430
- describe '#handle' do
430
+ describe '#absorb' do
431
431
  it 'returns success' do
432
- result = described_class.new.handle(url: 'https://#{test_url}')
432
+ result = described_class.new.absorb(url: 'https://#{test_url}')
433
433
  expect(result[:success]).to be true
434
434
  end
435
435
  end
@@ -1,9 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative '../definitions'
4
+
3
5
  module Legion
4
6
  module Extensions
5
7
  module Absorbers
6
8
  class Base
9
+ extend Legion::Extensions::Definitions
10
+
7
11
  attr_accessor :job_id, :runners
8
12
 
9
13
  class << self
@@ -21,10 +25,13 @@ module Legion
21
25
  end
22
26
  end
23
27
 
24
- def handle(url: nil, content: nil, metadata: {}, context: {})
25
- raise NotImplementedError, "#{self.class.name} must implement #handle"
28
+ def absorb(url: nil, content: nil, metadata: {}, context: {})
29
+ raise NotImplementedError, "#{self.class.name} must implement #absorb"
26
30
  end
27
31
 
32
+ # @deprecated Use #absorb instead
33
+ alias handle absorb
34
+
28
35
  def absorb_to_knowledge(content:, tags: [], scope: :global, **opts)
29
36
  return fallback_absorb(:chunker, content, tags, scope, opts) unless chunker_available?
30
37
  return fallback_absorb(:apollo, content, tags, scope, opts) unless apollo_available?
@@ -19,7 +19,7 @@ module Legion
19
19
 
20
20
  absorber = absorber_class.new
21
21
  absorber.job_id = job_id
22
- result = absorber.handle(url: input, content: context[:content],
22
+ result = absorber.absorb(url: input, content: context[:content],
23
23
  metadata: context[:metadata] || {}, context: context)
24
24
  publish_event("absorb.complete.#{job_id}", job_id: job_id, absorber: absorber_class.name,
25
25
  result: result)
@@ -1,11 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'dsl'
4
+
3
5
  module Legion
4
6
  module Extensions
5
7
  module Actors
6
8
  module Base
9
+ extend Legion::Extensions::Actors::Dsl
7
10
  include Legion::Extensions::Helpers::Lex
8
11
 
12
+ define_dsl_accessor :use_runner, default: true
13
+ define_dsl_accessor :check_subtask, default: true
14
+ define_dsl_accessor :generate_task, default: false
15
+ define_dsl_accessor :enabled, default: true
16
+ define_dsl_accessor :remote_invocable, default: true
17
+
9
18
  def runner
10
19
  Legion::Runner.run(runner_class: runner_class, function: function, check_subtask: check_subtask?, generate_task: generate_task?)
11
20
  rescue StandardError => e
@@ -29,8 +38,17 @@ module Legion
29
38
  nil
30
39
  end
31
40
 
41
+ def self.included(base)
42
+ base.extend(Legion::Extensions::Actors::Dsl) unless base.singleton_class.include?(Legion::Extensions::Actors::Dsl)
43
+ base.define_dsl_accessor(:use_runner, default: true) unless base.respond_to?(:use_runner)
44
+ base.define_dsl_accessor(:check_subtask, default: true) unless base.respond_to?(:check_subtask)
45
+ base.define_dsl_accessor(:generate_task, default: false) unless base.respond_to?(:generate_task)
46
+ base.define_dsl_accessor(:enabled, default: true) unless base.respond_to?(:enabled)
47
+ base.define_dsl_accessor(:remote_invocable, default: true) unless base.respond_to?(:remote_invocable)
48
+ end
49
+
32
50
  def use_runner?
33
- true
51
+ self.class.respond_to?(:use_runner) ? self.class.use_runner : true
34
52
  end
35
53
 
36
54
  def args
@@ -38,19 +56,19 @@ module Legion
38
56
  end
39
57
 
40
58
  def check_subtask?
41
- true
59
+ self.class.respond_to?(:check_subtask) ? self.class.check_subtask : true
42
60
  end
43
61
 
44
62
  def generate_task?
45
- false
63
+ self.class.respond_to?(:generate_task) ? self.class.generate_task : false
46
64
  end
47
65
 
48
66
  def enabled?
49
- true
67
+ self.class.respond_to?(:enabled) ? self.class.enabled : true
50
68
  end
51
69
 
52
70
  def remote_invocable?
53
- true
71
+ self.class.respond_to?(:remote_invocable) ? self.class.remote_invocable : true
54
72
  end
55
73
  end
56
74
  end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Actors
6
+ module Dsl
7
+ def define_dsl_accessor(name, default:)
8
+ define_singleton_method(name) do |val = :_unset|
9
+ if val == :_unset
10
+ if instance_variable_defined?(:"@#{name}")
11
+ instance_variable_get(:"@#{name}")
12
+ elsif superclass.respond_to?(name)
13
+ superclass.public_send(name)
14
+ else
15
+ default
16
+ end
17
+ else
18
+ instance_variable_set(:"@#{name}", val)
19
+ end
20
+ end
21
+
22
+ define_method(name) do
23
+ self.class.public_send(name)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -2,14 +2,20 @@
2
2
 
3
3
  require_relative 'base'
4
4
  require_relative 'fingerprint'
5
+ require_relative 'dsl'
5
6
 
6
7
  module Legion
7
8
  module Extensions
8
9
  module Actors
9
10
  class Every
11
+ extend Legion::Extensions::Actors::Dsl
10
12
  include Legion::Extensions::Actors::Base
11
13
  include Legion::Extensions::Actors::Fingerprint
12
14
 
15
+ define_dsl_accessor :time, default: 1
16
+ define_dsl_accessor :timeout, default: 5
17
+ define_dsl_accessor :run_now, default: false
18
+
13
19
  def initialize(**_opts)
14
20
  @timer = Concurrent::TimerTask.new(execution_interval: time, run_now: run_now?) do
15
21
  log.debug "[Every] tick: #{self.class}" if defined?(log)
@@ -25,16 +31,8 @@ module Legion
25
31
  log.log_exception(e, component_type: :actor)
26
32
  end
27
33
 
28
- def time
29
- 1
30
- end
31
-
32
- def timeout
33
- 5
34
- end
35
-
36
34
  def run_now?
37
- false
35
+ run_now
38
36
  end
39
37
 
40
38
  def action(**_opts)