lex-agentic-learning 0.1.3 → 0.1.5

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.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +15 -0
  3. data/Gemfile +2 -0
  4. data/lex-agentic-learning.gemspec +3 -2
  5. data/lib/legion/extensions/agentic/learning/anchoring/runners/anchoring.rb +2 -2
  6. data/lib/legion/extensions/agentic/learning/catalyst/runners/cognitive_catalyst.rb +2 -2
  7. data/lib/legion/extensions/agentic/learning/chrysalis/runners/cognitive_chrysalis.rb +1 -1
  8. data/lib/legion/extensions/agentic/learning/curiosity/runners/curiosity.rb +2 -2
  9. data/lib/legion/extensions/agentic/learning/epistemic_curiosity/runners/epistemic_curiosity.rb +2 -2
  10. data/lib/legion/extensions/agentic/learning/habit/runners/habit.rb +2 -2
  11. data/lib/legion/extensions/agentic/learning/hebbian/runners/hebbian_assembly.rb +2 -2
  12. data/lib/legion/extensions/agentic/learning/learning_rate/runners/learning_rate.rb +2 -2
  13. data/lib/legion/extensions/agentic/learning/meta_learning/runners/meta_learning.rb +2 -2
  14. data/lib/legion/extensions/agentic/learning/outcome_listener/actors/outcome_listener.rb +57 -0
  15. data/lib/legion/extensions/agentic/learning/outcome_listener/client.rb +47 -0
  16. data/lib/legion/extensions/agentic/learning/outcome_listener/helpers/constants.rb +22 -0
  17. data/lib/legion/extensions/agentic/learning/outcome_listener/helpers/domain_extractor.rb +37 -0
  18. data/lib/legion/extensions/agentic/learning/outcome_listener/helpers/lesson_builder.rb +45 -0
  19. data/lib/legion/extensions/agentic/learning/outcome_listener/runners/outcome_listener.rb +119 -0
  20. data/lib/legion/extensions/agentic/learning/outcome_listener.rb +3 -0
  21. data/lib/legion/extensions/agentic/learning/preference_learning/runners/preference_learning.rb +2 -2
  22. data/lib/legion/extensions/agentic/learning/procedural/runners/procedural_learning.rb +2 -2
  23. data/lib/legion/extensions/agentic/learning/scaffolding/runners/cognitive_scaffolding.rb +2 -2
  24. data/lib/legion/extensions/agentic/learning/version.rb +1 -1
  25. data/lib/legion/extensions/agentic/learning.rb +2 -1
  26. data/spec/legion/extensions/agentic/learning/outcome_listener/client_spec.rb +75 -0
  27. data/spec/legion/extensions/agentic/learning/outcome_listener/helpers/domain_extractor_spec.rb +55 -0
  28. data/spec/legion/extensions/agentic/learning/outcome_listener/helpers/lesson_builder_spec.rb +78 -0
  29. data/spec/legion/extensions/agentic/learning/outcome_listener/runners/outcome_listener_spec.rb +118 -0
  30. metadata +34 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c50a23830ef7370f63b760995907264a4df20b9dfe31355fb536d6a25d0b53a8
4
- data.tar.gz: a1d7dce23a94d8e5eefabd9fc4265195c0c8e1b42f50f6f4c724e2060f2b9d83
3
+ metadata.gz: 31725ad79b661a818e233bd94df4a06db34375f1d863e981ea41f5851fce4040
4
+ data.tar.gz: b63c48208160bbbbc292fd8a7563d51a53cc5a8bbed4d3cc9aac316bf19a6556
5
5
  SHA512:
6
- metadata.gz: 5b7893d82240aa11f85aad8544d883b1d24551c46b3ea2c19c9f8bf0c839968b90a3adb4a0d86f22ad63a807071d96ac6d24461729da6e8ed3b34b126ae807aa
7
- data.tar.gz: eaa5f171f8f87ed92f01748fb19c820f9fed1ae04048e920cd7f5b95dd4b443cca2c8e9092a2d1a5c83f0b66fec417367a4741b334a91a53358cec35ebdcd76c
6
+ metadata.gz: 93f6862f753e160b1fa73d2fe10627e765a5a4b85e8e504a3835addee1d4d1544e1081fff84db2ebd179bfce9ef085b1709f3c1bb1b804c65d17db8c30160019
7
+ data.tar.gz: af4930204eeaaf926ea5a20f01d97a7525412925fdfc90b4535f72b64cea433a687c2c878e55feb186bf177f6750571676860b4461d618c0635e3f1617f637cf
data/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.1.5] - 2026-03-31
4
+
5
+ ### Added
6
+ - OutcomeListener sub-module: subscription actor wiring task completion events to cognitive model updates (#4)
7
+ - DomainExtractor helper: extracts learning domain from runner class names
8
+ - LessonBuilder helper: generates structured situation-lesson pairs for Apollo persistence
9
+ - MetaLearning, LearningRate, and Scaffolding models updated on each task outcome
10
+ - Per-agent scoped lessons written to Apollo knowledge store
11
+ - Settings toggles: outcome_listener, write_to_apollo, min_lesson_severity
12
+
13
+ ## [0.1.4] - 2026-03-30
14
+
15
+ ### Changed
16
+ - update to rubocop-legion 0.1.7, resolve all offenses
17
+
3
18
  ## [0.1.3] - 2026-03-26
4
19
 
5
20
  ### Changed
data/Gemfile CHANGED
@@ -3,3 +3,5 @@
3
3
  source 'https://rubygems.org'
4
4
 
5
5
  gemspec
6
+
7
+ gem 'rubocop-legion'
@@ -33,6 +33,7 @@ Gem::Specification.new do |spec|
33
33
  spec.add_dependency 'legion-transport', '>= 1.3.9'
34
34
 
35
35
  spec.add_development_dependency 'rspec', '~> 3.13'
36
- spec.add_development_dependency 'rubocop', '~> 1.60'
37
- spec.add_development_dependency 'rubocop-rspec', '~> 2.26'
36
+ spec.add_development_dependency 'rubocop'
37
+ spec.add_development_dependency 'rubocop-legion'
38
+ spec.add_development_dependency 'rubocop-rspec'
38
39
  end
@@ -7,8 +7,8 @@ module Legion
7
7
  module Anchoring
8
8
  module Runners
9
9
  module Anchoring
10
- include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
11
- Legion::Extensions::Helpers.const_defined?(:Lex)
10
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
11
+ Legion::Extensions::Helpers.const_defined?(:Lex, false)
12
12
 
13
13
  def record_anchor(value:, domain: :general, **)
14
14
  anchor = anchor_store.add(value: value, domain: domain)
@@ -7,8 +7,8 @@ module Legion
7
7
  module Catalyst
8
8
  module Runners
9
9
  module CognitiveCatalyst
10
- include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
11
- Legion::Extensions::Helpers.const_defined?(:Lex)
10
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
11
+ Legion::Extensions::Helpers.const_defined?(:Lex, false)
12
12
 
13
13
  def create_catalyst(catalyst_type:, domain:, potency: nil, specificity: nil, engine: nil, **)
14
14
  e = engine || default_engine
@@ -11,7 +11,7 @@ module Legion
11
11
 
12
12
  begin
13
13
  include Legion::Extensions::Helpers::Lex # rubocop:disable Layout/EmptyLinesAfterModuleInclusion
14
- rescue StandardError
14
+ rescue StandardError => _e
15
15
  nil
16
16
  end
17
17
 
@@ -8,8 +8,8 @@ module Legion
8
8
  module Runners
9
9
  # Runner methods for the curiosity engine: gap detection, wonder lifecycle, agenda formation.
10
10
  module Curiosity
11
- include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
12
- Legion::Extensions::Helpers.const_defined?(:Lex)
11
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
12
+ Legion::Extensions::Helpers.const_defined?(:Lex, false)
13
13
 
14
14
  def detect_gaps(prior_results: {}, **)
15
15
  gaps = Helpers::GapDetector.detect(prior_results)
@@ -7,8 +7,8 @@ module Legion
7
7
  module EpistemicCuriosity
8
8
  module Runners
9
9
  module EpistemicCuriosity
10
- include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
11
- Legion::Extensions::Helpers.const_defined?(:Lex)
10
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
11
+ Legion::Extensions::Helpers.const_defined?(:Lex, false)
12
12
 
13
13
  def create_gap(question:, domain:, gap_type: :factual, urgency: Helpers::Constants::DEFAULT_URGENCY, **)
14
14
  result = engine.create_gap(question: question, domain: domain, gap_type: gap_type, urgency: urgency)
@@ -7,8 +7,8 @@ module Legion
7
7
  module Habit
8
8
  module Runners
9
9
  module Habit
10
- include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
11
- Legion::Extensions::Helpers.const_defined?(:Lex)
10
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
11
+ Legion::Extensions::Helpers.const_defined?(:Lex, false)
12
12
 
13
13
  def observe_action(action:, context: {}, **)
14
14
  log.debug "[habit] observe_action: action=#{action} context=#{context}"
@@ -7,8 +7,8 @@ module Legion
7
7
  module Hebbian
8
8
  module Runners
9
9
  module HebbianAssembly
10
- include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
11
- Legion::Extensions::Helpers.const_defined?(:Lex)
10
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
11
+ Legion::Extensions::Helpers.const_defined?(:Lex, false)
12
12
 
13
13
  def activate_unit(id:, level: 1.0, domain: :general, **)
14
14
  log.debug "[hebbian] activate: id=#{id} level=#{level}"
@@ -7,8 +7,8 @@ module Legion
7
7
  module LearningRate
8
8
  module Runners
9
9
  module LearningRate
10
- include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
11
- Legion::Extensions::Helpers.const_defined?(:Lex)
10
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
11
+ Legion::Extensions::Helpers.const_defined?(:Lex, false)
12
12
 
13
13
  def record_prediction(correct:, domain: :general, **)
14
14
  rate_model.record_prediction(domain: domain, correct: correct)
@@ -7,8 +7,8 @@ module Legion
7
7
  module MetaLearning
8
8
  module Runners
9
9
  module MetaLearning
10
- include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
11
- Legion::Extensions::Helpers.const_defined?(:Lex)
10
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
11
+ Legion::Extensions::Helpers.const_defined?(:Lex, false)
12
12
 
13
13
  def create_learning_domain(name:, learning_rate: Helpers::Constants::DEFAULT_LEARNING_RATE,
14
14
  related_domains: [], **)
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ return unless defined?(Legion::Extensions::Actors::Subscription)
4
+
5
+ module Legion
6
+ module Extensions
7
+ module Agentic
8
+ module Learning
9
+ module OutcomeListener
10
+ module Actor
11
+ class OutcomeListener < Legion::Extensions::Actors::Subscription
12
+ def runner_class
13
+ Legion::Extensions::Agentic::Learning::OutcomeListener::Runners::OutcomeListener
14
+ end
15
+
16
+ def runner_function
17
+ 'process_outcome'
18
+ end
19
+
20
+ def check_subtask?
21
+ false
22
+ end
23
+
24
+ def generate_task?
25
+ false
26
+ end
27
+
28
+ def use_runner?
29
+ false
30
+ end
31
+
32
+ def enabled? # rubocop:disable Legion/Extension/ActorEnabledSideEffects
33
+ outcome_listener_setting? &&
34
+ defined?(Legion::Extensions::Agentic::Learning::OutcomeListener::Runners::OutcomeListener) &&
35
+ transport_connected?
36
+ rescue StandardError => e
37
+ log.warn "[outcome_listener] enabled? check failed: #{e.message}"
38
+ false
39
+ end
40
+
41
+ private
42
+
43
+ def outcome_listener_setting?
44
+ return true unless defined?(Legion::Settings)
45
+
46
+ Legion::Settings.dig(:agentic, :learning, :outcome_listener) != false
47
+ rescue StandardError => e
48
+ log.warn "[outcome_listener] settings check failed: #{e.message}"
49
+ true
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/agentic/learning/outcome_listener/helpers/constants'
4
+ require 'legion/extensions/agentic/learning/outcome_listener/helpers/domain_extractor'
5
+ require 'legion/extensions/agentic/learning/outcome_listener/helpers/lesson_builder'
6
+ require 'legion/extensions/agentic/learning/outcome_listener/runners/outcome_listener'
7
+
8
+ module Legion
9
+ module Extensions
10
+ module Agentic
11
+ module Learning
12
+ module OutcomeListener
13
+ class Client
14
+ include Runners::OutcomeListener
15
+
16
+ # rubocop:disable ThreadSafety/ClassInstanceVariable
17
+ class << self
18
+ def meta_client
19
+ @meta_client ||= MetaLearning::Client.new
20
+ end
21
+
22
+ def scaffolding_client
23
+ @scaffolding_client ||= Scaffolding::Client.new
24
+ end
25
+
26
+ def learning_rate_client
27
+ @learning_rate_client ||= LearningRate::Client.new
28
+ end
29
+
30
+ def domain_map
31
+ @domain_map ||= {}
32
+ end
33
+
34
+ def reset!
35
+ @meta_client = nil
36
+ @scaffolding_client = nil
37
+ @learning_rate_client = nil
38
+ @domain_map = nil
39
+ end
40
+ end
41
+ # rubocop:enable ThreadSafety/ClassInstanceVariable
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Agentic
6
+ module Learning
7
+ module OutcomeListener
8
+ module Helpers
9
+ module Constants
10
+ DEFAULT_MIN_LESSON_SEVERITY = 0.3
11
+ COMPLETED_STATUS = 'task.completed'
12
+ FAILED_STATUS = 'task.failed'
13
+ DEFAULT_DIFFICULTY = 0.5
14
+ DEFAULT_CONFIDENCE_SUCCESS = 0.7
15
+ DEFAULT_CONFIDENCE_FAILURE = 0.5
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Agentic
6
+ module Learning
7
+ module OutcomeListener
8
+ module Helpers
9
+ module DomainExtractor
10
+ RUNNER_PATTERN = /Legion::Extensions::(?:Agentic::)?(\w+)/
11
+
12
+ module_function
13
+
14
+ def extract(runner_class_name)
15
+ return 'unknown' if runner_class_name.nil? || runner_class_name.empty?
16
+
17
+ match = runner_class_name.match(RUNNER_PATTERN)
18
+ return snake_case(match[1]) if match
19
+
20
+ segments = runner_class_name.split('::')
21
+ return snake_case(segments[-2]) if segments.size >= 2
22
+
23
+ snake_case(segments.last)
24
+ end
25
+
26
+ def snake_case(str)
27
+ str.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
28
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
29
+ .downcase
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Agentic
6
+ module Learning
7
+ module OutcomeListener
8
+ module Helpers
9
+ module LessonBuilder
10
+ include Constants
11
+
12
+ module_function
13
+
14
+ def build(runner_class:, domain:, success:, status: nil, function: nil, source_agent: nil) # rubocop:disable Lint/UnusedMethodArgument
15
+ {
16
+ situation: build_situation(runner_class, function),
17
+ outcome: success ? :success : :failure,
18
+ lesson: build_lesson(domain, success),
19
+ domain: domain,
20
+ confidence: success ? DEFAULT_CONFIDENCE_SUCCESS : DEFAULT_CONFIDENCE_FAILURE,
21
+ source_agent: source_agent,
22
+ recorded_at: Time.now.utc
23
+ }
24
+ end
25
+
26
+ def build_situation(runner_class, function)
27
+ parts = [runner_class.to_s]
28
+ parts << "##{function}" if function
29
+ parts.join
30
+ end
31
+
32
+ def build_lesson(domain, success)
33
+ if success
34
+ "Task in domain '#{domain}' completed successfully"
35
+ else
36
+ "Task in domain '#{domain}' failed — review for corrective action"
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Agentic
6
+ module Learning
7
+ module OutcomeListener
8
+ module Runners
9
+ module OutcomeListener
10
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
11
+ Legion::Extensions::Helpers.const_defined?(:Lex, false)
12
+
13
+ def process_outcome(payload = {}, **)
14
+ runner_class_name = payload[:runner_class].to_s
15
+ status = payload[:status].to_s
16
+ domain = Helpers::DomainExtractor.extract(runner_class_name)
17
+ success = status == Helpers::Constants::COMPLETED_STATUS
18
+ source_agent = payload[:source_agent] || payload[:agent_id]
19
+
20
+ updates = {}
21
+ updates[:meta_learning] = update_meta_learning_model(domain, success)
22
+ updates[:learning_rate] = update_learning_rate_model(domain, success)
23
+ updates[:scaffolding] = update_scaffolding_model(domain, success, payload)
24
+
25
+ if write_to_apollo?
26
+ lesson = Helpers::LessonBuilder.build(
27
+ runner_class: runner_class_name,
28
+ function: payload[:function],
29
+ status: status,
30
+ domain: domain,
31
+ success: success,
32
+ source_agent: source_agent
33
+ )
34
+ write_apollo_lesson(lesson, source_agent) if lesson[:confidence] >= min_lesson_severity
35
+ updates[:lesson] = lesson
36
+ end
37
+
38
+ log.debug "[outcome_listener] domain=#{domain} success=#{success} updates=#{updates.keys}"
39
+ { success: true, domain: domain, outcome: success, updates: updates }
40
+ end
41
+
42
+ private
43
+
44
+ def update_meta_learning_model(domain, success)
45
+ meta = self.class.meta_client
46
+ domain_id = resolve_domain_id(meta, domain)
47
+ meta.record_learning_episode(domain_id: domain_id, success: success)
48
+ rescue StandardError => e
49
+ log.warn "[outcome_listener] meta_learning update failed: #{e.message}"
50
+ { error: e.message }
51
+ end
52
+
53
+ def update_learning_rate_model(domain, success)
54
+ lr = self.class.learning_rate_client
55
+ lr.record_prediction(correct: success, domain: domain.to_sym)
56
+ rescue StandardError => e
57
+ log.warn "[outcome_listener] learning_rate update failed: #{e.message}"
58
+ { error: e.message }
59
+ end
60
+
61
+ def update_scaffolding_model(domain, success, payload)
62
+ sc = self.class.scaffolding_client
63
+ scaffolds = sc.engine.by_domain(domain: domain)
64
+ return { skipped: true, reason: :no_scaffold } if scaffolds.empty?
65
+
66
+ scaffold = scaffolds.first
67
+ difficulty = payload[:complexity]&.to_f || Helpers::Constants::DEFAULT_DIFFICULTY
68
+ sc.attempt_scaffolded_task(scaffold_id: scaffold.id, difficulty: difficulty, success: success)
69
+ rescue StandardError => e
70
+ log.warn "[outcome_listener] scaffolding update failed: #{e.message}"
71
+ { error: e.message }
72
+ end
73
+
74
+ def resolve_domain_id(meta, domain)
75
+ map = self.class.domain_map
76
+ return map[domain] if map.key?(domain)
77
+
78
+ result = meta.create_learning_domain(name: domain)
79
+ map[domain] = result[:id]
80
+ result[:id]
81
+ end
82
+
83
+ def write_apollo_lesson(lesson, source_agent)
84
+ return unless defined?(Legion::Apollo)
85
+
86
+ ingest_knowledge(content: json_generate(lesson),
87
+ content_type: 'task_outcome_lesson',
88
+ tags: ['task_outcome', lesson[:domain]],
89
+ source_agent: source_agent,
90
+ knowledge_domain: 'learning')
91
+ rescue StandardError => e
92
+ log.warn "[outcome_listener] apollo write failed: #{e.message}"
93
+ end
94
+
95
+ def write_to_apollo?
96
+ return false unless defined?(Legion::Settings)
97
+
98
+ Legion::Settings.dig(:agentic, :learning, :write_to_apollo) != false
99
+ rescue StandardError => e
100
+ log.warn "[outcome_listener] write_to_apollo? check failed: #{e.message}"
101
+ true
102
+ end
103
+
104
+ def min_lesson_severity
105
+ return Helpers::Constants::DEFAULT_MIN_LESSON_SEVERITY unless defined?(Legion::Settings)
106
+
107
+ Legion::Settings.dig(:agentic, :learning, :min_lesson_severity) ||
108
+ Helpers::Constants::DEFAULT_MIN_LESSON_SEVERITY
109
+ rescue StandardError => e
110
+ log.warn "[outcome_listener] min_lesson_severity check failed: #{e.message}"
111
+ Helpers::Constants::DEFAULT_MIN_LESSON_SEVERITY
112
+ end
113
+ end
114
+ end
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'outcome_listener/client'
@@ -7,8 +7,8 @@ module Legion
7
7
  module PreferenceLearning
8
8
  module Runners
9
9
  module PreferenceLearning
10
- include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
11
- Legion::Extensions::Helpers.const_defined?(:Lex)
10
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
11
+ Legion::Extensions::Helpers.const_defined?(:Lex, false)
12
12
 
13
13
  def register_preference_option(label:, domain: :general, **)
14
14
  result = preference_engine.register_option(label: label, domain: domain)
@@ -7,8 +7,8 @@ module Legion
7
7
  module Procedural
8
8
  module Runners
9
9
  module ProceduralLearning
10
- include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
11
- Legion::Extensions::Helpers.const_defined?(:Lex)
10
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
11
+ Legion::Extensions::Helpers.const_defined?(:Lex, false)
12
12
 
13
13
  def create_skill(name:, domain:, **)
14
14
  skill = engine.create_skill(name: name, domain: domain)
@@ -7,8 +7,8 @@ module Legion
7
7
  module Scaffolding
8
8
  module Runners
9
9
  module CognitiveScaffolding
10
- include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers) &&
11
- Legion::Extensions::Helpers.const_defined?(:Lex)
10
+ include Legion::Extensions::Helpers::Lex if Legion::Extensions.const_defined?(:Helpers, false) &&
11
+ Legion::Extensions::Helpers.const_defined?(:Lex, false)
12
12
 
13
13
  def create_scaffold(skill_name:, domain:, competence: nil, **)
14
14
  comp = competence || Helpers::Constants::DEFAULT_COMPETENCE
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Agentic
6
6
  module Learning
7
- VERSION = '0.1.3'
7
+ VERSION = '0.1.5'
8
8
  end
9
9
  end
10
10
  end
@@ -15,12 +15,13 @@ require_relative 'learning/meta_learning'
15
15
  require_relative 'learning/preference_learning'
16
16
  require_relative 'learning/procedural'
17
17
  require_relative 'learning/anchoring'
18
+ require_relative 'learning/outcome_listener'
18
19
 
19
20
  module Legion
20
21
  module Extensions
21
22
  module Agentic
22
23
  module Learning
23
- extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core
24
+ extend Legion::Extensions::Core if Legion::Extensions.const_defined? :Core, false
24
25
 
25
26
  def self.remote_invocable?
26
27
  false
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/agentic/learning/outcome_listener/client'
4
+
5
+ RSpec.describe Legion::Extensions::Agentic::Learning::OutcomeListener::Client do
6
+ before { described_class.reset! }
7
+
8
+ it 'responds to process_outcome' do
9
+ client = described_class.new
10
+ expect(client).to respond_to(:process_outcome)
11
+ end
12
+
13
+ describe '.meta_client' do
14
+ it 'returns a MetaLearning::Client' do
15
+ expect(described_class.meta_client).to be_a(
16
+ Legion::Extensions::Agentic::Learning::MetaLearning::Client
17
+ )
18
+ end
19
+
20
+ it 'returns the same instance on repeated calls' do
21
+ expect(described_class.meta_client).to equal(described_class.meta_client)
22
+ end
23
+ end
24
+
25
+ describe '.scaffolding_client' do
26
+ it 'returns a Scaffolding::Client' do
27
+ expect(described_class.scaffolding_client).to be_a(
28
+ Legion::Extensions::Agentic::Learning::Scaffolding::Client
29
+ )
30
+ end
31
+
32
+ it 'returns the same instance on repeated calls' do
33
+ expect(described_class.scaffolding_client).to equal(described_class.scaffolding_client)
34
+ end
35
+ end
36
+
37
+ describe '.learning_rate_client' do
38
+ it 'returns a LearningRate::Client' do
39
+ expect(described_class.learning_rate_client).to be_a(
40
+ Legion::Extensions::Agentic::Learning::LearningRate::Client
41
+ )
42
+ end
43
+
44
+ it 'returns the same instance on repeated calls' do
45
+ expect(described_class.learning_rate_client).to equal(described_class.learning_rate_client)
46
+ end
47
+ end
48
+
49
+ describe '.domain_map' do
50
+ it 'returns an empty hash initially' do
51
+ expect(described_class.domain_map).to eq({})
52
+ end
53
+
54
+ it 'persists entries across calls' do
55
+ described_class.domain_map['test'] = 'id-123'
56
+ expect(described_class.domain_map['test']).to eq('id-123')
57
+ end
58
+ end
59
+
60
+ describe '.reset!' do
61
+ it 'clears all shared state' do
62
+ described_class.meta_client
63
+ described_class.scaffolding_client
64
+ described_class.learning_rate_client
65
+ described_class.domain_map['test'] = 'id'
66
+
67
+ described_class.reset!
68
+
69
+ expect(described_class.domain_map).to eq({})
70
+ expect(described_class.instance_variable_get(:@meta_client)).to be_nil
71
+ expect(described_class.instance_variable_get(:@scaffolding_client)).to be_nil
72
+ expect(described_class.instance_variable_get(:@learning_rate_client)).to be_nil
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/agentic/learning/outcome_listener/helpers/constants'
4
+ require 'legion/extensions/agentic/learning/outcome_listener/helpers/domain_extractor'
5
+
6
+ RSpec.describe Legion::Extensions::Agentic::Learning::OutcomeListener::Helpers::DomainExtractor do
7
+ describe '.extract' do
8
+ it 'extracts domain from standard runner class' do
9
+ expect(described_class.extract('Legion::Extensions::Http::Runners::Get')).to eq('http')
10
+ end
11
+
12
+ it 'extracts domain from agentic runner class' do
13
+ expect(described_class.extract('Legion::Extensions::Agentic::Learning::MetaLearning::Runners::MetaLearning'))
14
+ .to eq('learning')
15
+ end
16
+
17
+ it 'handles consul extension' do
18
+ expect(described_class.extract('Legion::Extensions::Consul::Runners::Kv')).to eq('consul')
19
+ end
20
+
21
+ it 'handles camel case domains' do
22
+ expect(described_class.extract('Legion::Extensions::SwarmGithub::Runners::Issue')).to eq('swarm_github')
23
+ end
24
+
25
+ it 'returns unknown for nil input' do
26
+ expect(described_class.extract(nil)).to eq('unknown')
27
+ end
28
+
29
+ it 'returns unknown for empty string' do
30
+ expect(described_class.extract('')).to eq('unknown')
31
+ end
32
+
33
+ it 'falls back to second-to-last segment for non-standard format' do
34
+ expect(described_class.extract('Custom::Runner::DoWork')).to eq('runner')
35
+ end
36
+
37
+ it 'handles single segment' do
38
+ expect(described_class.extract('Worker')).to eq('worker')
39
+ end
40
+ end
41
+
42
+ describe '.snake_case' do
43
+ it 'converts CamelCase to snake_case' do
44
+ expect(described_class.snake_case('SwarmGithub')).to eq('swarm_github')
45
+ end
46
+
47
+ it 'converts simple word' do
48
+ expect(described_class.snake_case('Http')).to eq('http')
49
+ end
50
+
51
+ it 'handles consecutive capitals' do
52
+ expect(described_class.snake_case('LLMGateway')).to eq('llm_gateway')
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/agentic/learning/outcome_listener/helpers/constants'
4
+ require 'legion/extensions/agentic/learning/outcome_listener/helpers/lesson_builder'
5
+
6
+ RSpec.describe Legion::Extensions::Agentic::Learning::OutcomeListener::Helpers::LessonBuilder do
7
+ describe '.build' do
8
+ let(:success_lesson) do
9
+ described_class.build(
10
+ runner_class: 'Legion::Extensions::Http::Runners::Get',
11
+ function: 'fetch',
12
+ status: 'task.completed',
13
+ domain: 'http',
14
+ success: true,
15
+ source_agent: 'agent-1'
16
+ )
17
+ end
18
+
19
+ let(:failure_lesson) do
20
+ described_class.build(
21
+ runner_class: 'Legion::Extensions::Consul::Runners::Kv',
22
+ function: 'put',
23
+ status: 'task.failed',
24
+ domain: 'consul',
25
+ success: false,
26
+ source_agent: 'agent-2'
27
+ )
28
+ end
29
+
30
+ it 'builds a success lesson with correct outcome' do
31
+ expect(success_lesson[:outcome]).to eq(:success)
32
+ end
33
+
34
+ it 'builds a failure lesson with correct outcome' do
35
+ expect(failure_lesson[:outcome]).to eq(:failure)
36
+ end
37
+
38
+ it 'includes situation with runner class and function' do
39
+ expect(success_lesson[:situation]).to eq('Legion::Extensions::Http::Runners::Get#fetch')
40
+ end
41
+
42
+ it 'includes domain' do
43
+ expect(success_lesson[:domain]).to eq('http')
44
+ end
45
+
46
+ it 'assigns higher confidence to successes' do
47
+ expect(success_lesson[:confidence]).to be > failure_lesson[:confidence]
48
+ end
49
+
50
+ it 'includes source_agent' do
51
+ expect(success_lesson[:source_agent]).to eq('agent-1')
52
+ end
53
+
54
+ it 'includes recorded_at timestamp' do
55
+ expect(success_lesson[:recorded_at]).to be_a(Time)
56
+ end
57
+
58
+ it 'builds lesson text for success' do
59
+ expect(success_lesson[:lesson]).to include('completed successfully')
60
+ end
61
+
62
+ it 'builds lesson text for failure' do
63
+ expect(failure_lesson[:lesson]).to include('failed')
64
+ end
65
+ end
66
+
67
+ describe '.build_situation' do
68
+ it 'combines runner_class and function' do
69
+ result = described_class.build_situation('MyRunner', 'do_work')
70
+ expect(result).to eq('MyRunner#do_work')
71
+ end
72
+
73
+ it 'omits function when nil' do
74
+ result = described_class.build_situation('MyRunner', nil)
75
+ expect(result).to eq('MyRunner')
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'legion/extensions/agentic/learning/outcome_listener/client'
4
+
5
+ RSpec.describe Legion::Extensions::Agentic::Learning::OutcomeListener::Runners::OutcomeListener do
6
+ let(:client) { Legion::Extensions::Agentic::Learning::OutcomeListener::Client.new }
7
+
8
+ before { Legion::Extensions::Agentic::Learning::OutcomeListener::Client.reset! }
9
+
10
+ let(:completed_payload) do
11
+ {
12
+ runner_class: 'Legion::Extensions::Http::Runners::Get',
13
+ function: 'fetch',
14
+ status: 'task.completed',
15
+ source_agent: 'agent-1'
16
+ }
17
+ end
18
+
19
+ let(:failed_payload) do
20
+ {
21
+ runner_class: 'Legion::Extensions::Consul::Runners::Kv',
22
+ function: 'put',
23
+ status: 'task.failed',
24
+ source_agent: 'agent-2'
25
+ }
26
+ end
27
+
28
+ describe '#process_outcome' do
29
+ it 'returns success hash for completed task' do
30
+ result = client.process_outcome(completed_payload)
31
+ expect(result[:success]).to be true
32
+ expect(result[:domain]).to eq('http')
33
+ expect(result[:outcome]).to be true
34
+ end
35
+
36
+ it 'returns success hash for failed task' do
37
+ result = client.process_outcome(failed_payload)
38
+ expect(result[:success]).to be true
39
+ expect(result[:domain]).to eq('consul')
40
+ expect(result[:outcome]).to be false
41
+ end
42
+
43
+ it 'updates meta_learning model on completion' do
44
+ result = client.process_outcome(completed_payload)
45
+ episode = result[:updates][:meta_learning]
46
+ expect(episode[:success]).to be true
47
+ expect(episode[:domain_name]).to eq('http')
48
+ end
49
+
50
+ it 'updates meta_learning model on failure' do
51
+ result = client.process_outcome(failed_payload)
52
+ episode = result[:updates][:meta_learning]
53
+ expect(episode[:success]).to be false
54
+ end
55
+
56
+ it 'updates learning_rate model' do
57
+ result = client.process_outcome(completed_payload)
58
+ lr = result[:updates][:learning_rate]
59
+ expect(lr[:success]).to be true
60
+ expect(lr[:domain]).to eq(:http)
61
+ end
62
+
63
+ it 'skips scaffolding when no scaffold exists for domain' do
64
+ result = client.process_outcome(completed_payload)
65
+ scaffolding = result[:updates][:scaffolding]
66
+ expect(scaffolding[:skipped]).to be true
67
+ expect(scaffolding[:reason]).to eq(:no_scaffold)
68
+ end
69
+
70
+ it 'updates scaffolding when scaffold exists for domain' do
71
+ sc = Legion::Extensions::Agentic::Learning::OutcomeListener::Client.scaffolding_client
72
+ sc.create_scaffold(skill_name: 'http_requests', domain: 'http')
73
+ result = client.process_outcome(completed_payload)
74
+ scaffolding = result[:updates][:scaffolding]
75
+ expect(scaffolding[:success]).to be true
76
+ end
77
+
78
+ it 'uses payload complexity for scaffolding difficulty' do
79
+ sc = Legion::Extensions::Agentic::Learning::OutcomeListener::Client.scaffolding_client
80
+ scaffold_result = sc.create_scaffold(skill_name: 'consul_kv', domain: 'consul')
81
+ original_competence = scaffold_result[:scaffold][:competence]
82
+
83
+ client.process_outcome(failed_payload.merge(complexity: 0.9))
84
+ scaffolding_client = Legion::Extensions::Agentic::Learning::OutcomeListener::Client.scaffolding_client
85
+ scaffold = scaffolding_client.engine.by_domain(domain: 'consul').first
86
+ expect(scaffold.competence).to be < original_competence
87
+ end
88
+
89
+ it 'creates domain in meta_learning on first encounter' do
90
+ client.process_outcome(completed_payload)
91
+ meta = Legion::Extensions::Agentic::Learning::OutcomeListener::Client.meta_client
92
+ stats = meta.meta_learning_stats
93
+ expect(stats[:domain_count]).to eq(1)
94
+ end
95
+
96
+ it 'reuses existing domain on subsequent outcomes' do
97
+ client.process_outcome(completed_payload)
98
+ client.process_outcome(completed_payload)
99
+ meta = Legion::Extensions::Agentic::Learning::OutcomeListener::Client.meta_client
100
+ stats = meta.meta_learning_stats
101
+ expect(stats[:domain_count]).to eq(1)
102
+ end
103
+
104
+ it 'accumulates proficiency across multiple successes' do
105
+ 3.times { client.process_outcome(completed_payload) }
106
+ meta = Legion::Extensions::Agentic::Learning::OutcomeListener::Client.meta_client
107
+ domain_id = Legion::Extensions::Agentic::Learning::OutcomeListener::Client.domain_map['http']
108
+ episodes = meta.send(:engine).domains[domain_id]
109
+ expect(episodes.proficiency).to be > 0.0
110
+ end
111
+
112
+ it 'handles empty payload gracefully' do
113
+ result = client.process_outcome({})
114
+ expect(result[:success]).to be true
115
+ expect(result[:domain]).to eq('unknown')
116
+ end
117
+ end
118
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lex-agentic-learning
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -125,30 +125,44 @@ dependencies:
125
125
  name: rubocop
126
126
  requirement: !ruby/object:Gem::Requirement
127
127
  requirements:
128
- - - "~>"
128
+ - - ">="
129
129
  - !ruby/object:Gem::Version
130
- version: '1.60'
130
+ version: '0'
131
131
  type: :development
132
132
  prerelease: false
133
133
  version_requirements: !ruby/object:Gem::Requirement
134
134
  requirements:
135
- - - "~>"
135
+ - - ">="
136
136
  - !ruby/object:Gem::Version
137
- version: '1.60'
137
+ version: '0'
138
+ - !ruby/object:Gem::Dependency
139
+ name: rubocop-legion
140
+ requirement: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ type: :development
146
+ prerelease: false
147
+ version_requirements: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: '0'
138
152
  - !ruby/object:Gem::Dependency
139
153
  name: rubocop-rspec
140
154
  requirement: !ruby/object:Gem::Requirement
141
155
  requirements:
142
- - - "~>"
156
+ - - ">="
143
157
  - !ruby/object:Gem::Version
144
- version: '2.26'
158
+ version: '0'
145
159
  type: :development
146
160
  prerelease: false
147
161
  version_requirements: !ruby/object:Gem::Requirement
148
162
  requirements:
149
- - - "~>"
163
+ - - ">="
150
164
  - !ruby/object:Gem::Version
151
- version: '2.26'
165
+ version: '0'
152
166
  description: 'LEX agentic learning domain: adaptation, memory consolidation, skill
153
167
  acquisition'
154
168
  email:
@@ -239,6 +253,13 @@ files:
239
253
  - lib/legion/extensions/agentic/learning/meta_learning/helpers/strategy.rb
240
254
  - lib/legion/extensions/agentic/learning/meta_learning/runners/meta_learning.rb
241
255
  - lib/legion/extensions/agentic/learning/meta_learning/version.rb
256
+ - lib/legion/extensions/agentic/learning/outcome_listener.rb
257
+ - lib/legion/extensions/agentic/learning/outcome_listener/actors/outcome_listener.rb
258
+ - lib/legion/extensions/agentic/learning/outcome_listener/client.rb
259
+ - lib/legion/extensions/agentic/learning/outcome_listener/helpers/constants.rb
260
+ - lib/legion/extensions/agentic/learning/outcome_listener/helpers/domain_extractor.rb
261
+ - lib/legion/extensions/agentic/learning/outcome_listener/helpers/lesson_builder.rb
262
+ - lib/legion/extensions/agentic/learning/outcome_listener/runners/outcome_listener.rb
242
263
  - lib/legion/extensions/agentic/learning/plasticity.rb
243
264
  - lib/legion/extensions/agentic/learning/plasticity/client.rb
244
265
  - lib/legion/extensions/agentic/learning/plasticity/helpers/constants.rb
@@ -327,6 +348,10 @@ files:
327
348
  - spec/legion/extensions/agentic/learning/meta_learning/helpers/meta_learning_engine_spec.rb
328
349
  - spec/legion/extensions/agentic/learning/meta_learning/helpers/strategy_spec.rb
329
350
  - spec/legion/extensions/agentic/learning/meta_learning/runners/meta_learning_spec.rb
351
+ - spec/legion/extensions/agentic/learning/outcome_listener/client_spec.rb
352
+ - spec/legion/extensions/agentic/learning/outcome_listener/helpers/domain_extractor_spec.rb
353
+ - spec/legion/extensions/agentic/learning/outcome_listener/helpers/lesson_builder_spec.rb
354
+ - spec/legion/extensions/agentic/learning/outcome_listener/runners/outcome_listener_spec.rb
330
355
  - spec/legion/extensions/agentic/learning/plasticity/helpers/constants_spec.rb
331
356
  - spec/legion/extensions/agentic/learning/plasticity/helpers/neural_pathway_spec.rb
332
357
  - spec/legion/extensions/agentic/learning/plasticity/helpers/plasticity_engine_spec.rb