lex-agentic-learning 0.1.4 → 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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0fa94d9402624e09dc616349b7cc9e9e722d6ff899922e13ca93baa2503700da
4
- data.tar.gz: ba91d298fb98bf14dd32f67aa3a00799273a39f4e8a43fbedfd9caacc427b04c
3
+ metadata.gz: 31725ad79b661a818e233bd94df4a06db34375f1d863e981ea41f5851fce4040
4
+ data.tar.gz: b63c48208160bbbbc292fd8a7563d51a53cc5a8bbed4d3cc9aac316bf19a6556
5
5
  SHA512:
6
- metadata.gz: 60551f68956f4fb815f1ce84d12763023d63f0eb3dd2c3d1d4d3e237f6b6647f2b628a1b2ef6bc59e6d2d04a438115e2857b2d84b7c0e88472b821bfb7e7b0e9
7
- data.tar.gz: 57ca51784f9441096a08a844c5289ff1503936aef7c153ad7a286aac688cf25057c18e0a21ee32ba3fb7c058fdd031408d0aa5764355da556b8dcd6131d79631
6
+ metadata.gz: 93f6862f753e160b1fa73d2fe10627e765a5a4b85e8e504a3835addee1d4d1544e1081fff84db2ebd179bfce9ef085b1709f3c1bb1b804c65d17db8c30160019
7
+ data.tar.gz: af4930204eeaaf926ea5a20f01d97a7525412925fdfc90b4535f72b64cea433a687c2c878e55feb186bf177f6750571676860b4461d618c0635e3f1617f637cf
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
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
+
3
13
  ## [0.1.4] - 2026-03-30
4
14
 
5
15
  ### 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,7 +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-legion', '~> 0.1'
38
- 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'
39
39
  end
@@ -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'
@@ -4,7 +4,7 @@ module Legion
4
4
  module Extensions
5
5
  module Agentic
6
6
  module Learning
7
- VERSION = '0.1.4'
7
+ VERSION = '0.1.5'
8
8
  end
9
9
  end
10
10
  end
@@ -15,6 +15,7 @@ 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
@@ -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.4
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -125,44 +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
138
  - !ruby/object:Gem::Dependency
139
139
  name: rubocop-legion
140
140
  requirement: !ruby/object:Gem::Requirement
141
141
  requirements:
142
- - - "~>"
142
+ - - ">="
143
143
  - !ruby/object:Gem::Version
144
- version: '0.1'
144
+ version: '0'
145
145
  type: :development
146
146
  prerelease: false
147
147
  version_requirements: !ruby/object:Gem::Requirement
148
148
  requirements:
149
- - - "~>"
149
+ - - ">="
150
150
  - !ruby/object:Gem::Version
151
- version: '0.1'
151
+ version: '0'
152
152
  - !ruby/object:Gem::Dependency
153
153
  name: rubocop-rspec
154
154
  requirement: !ruby/object:Gem::Requirement
155
155
  requirements:
156
- - - "~>"
156
+ - - ">="
157
157
  - !ruby/object:Gem::Version
158
- version: '2.26'
158
+ version: '0'
159
159
  type: :development
160
160
  prerelease: false
161
161
  version_requirements: !ruby/object:Gem::Requirement
162
162
  requirements:
163
- - - "~>"
163
+ - - ">="
164
164
  - !ruby/object:Gem::Version
165
- version: '2.26'
165
+ version: '0'
166
166
  description: 'LEX agentic learning domain: adaptation, memory consolidation, skill
167
167
  acquisition'
168
168
  email:
@@ -253,6 +253,13 @@ files:
253
253
  - lib/legion/extensions/agentic/learning/meta_learning/helpers/strategy.rb
254
254
  - lib/legion/extensions/agentic/learning/meta_learning/runners/meta_learning.rb
255
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
256
263
  - lib/legion/extensions/agentic/learning/plasticity.rb
257
264
  - lib/legion/extensions/agentic/learning/plasticity/client.rb
258
265
  - lib/legion/extensions/agentic/learning/plasticity/helpers/constants.rb
@@ -341,6 +348,10 @@ files:
341
348
  - spec/legion/extensions/agentic/learning/meta_learning/helpers/meta_learning_engine_spec.rb
342
349
  - spec/legion/extensions/agentic/learning/meta_learning/helpers/strategy_spec.rb
343
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
344
355
  - spec/legion/extensions/agentic/learning/plasticity/helpers/constants_spec.rb
345
356
  - spec/legion/extensions/agentic/learning/plasticity/helpers/neural_pathway_spec.rb
346
357
  - spec/legion/extensions/agentic/learning/plasticity/helpers/plasticity_engine_spec.rb