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 +4 -4
- data/CHANGELOG.md +10 -0
- data/Gemfile +2 -0
- data/lex-agentic-learning.gemspec +3 -3
- data/lib/legion/extensions/agentic/learning/outcome_listener/actors/outcome_listener.rb +57 -0
- data/lib/legion/extensions/agentic/learning/outcome_listener/client.rb +47 -0
- data/lib/legion/extensions/agentic/learning/outcome_listener/helpers/constants.rb +22 -0
- data/lib/legion/extensions/agentic/learning/outcome_listener/helpers/domain_extractor.rb +37 -0
- data/lib/legion/extensions/agentic/learning/outcome_listener/helpers/lesson_builder.rb +45 -0
- data/lib/legion/extensions/agentic/learning/outcome_listener/runners/outcome_listener.rb +119 -0
- data/lib/legion/extensions/agentic/learning/outcome_listener.rb +3 -0
- data/lib/legion/extensions/agentic/learning/version.rb +1 -1
- data/lib/legion/extensions/agentic/learning.rb +1 -0
- data/spec/legion/extensions/agentic/learning/outcome_listener/client_spec.rb +75 -0
- data/spec/legion/extensions/agentic/learning/outcome_listener/helpers/domain_extractor_spec.rb +55 -0
- data/spec/legion/extensions/agentic/learning/outcome_listener/helpers/lesson_builder_spec.rb +78 -0
- data/spec/legion/extensions/agentic/learning/outcome_listener/runners/outcome_listener_spec.rb +118 -0
- metadata +24 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 31725ad79b661a818e233bd94df4a06db34375f1d863e981ea41f5851fce4040
|
|
4
|
+
data.tar.gz: b63c48208160bbbbc292fd8a7563d51a53cc5a8bbed4d3cc9aac316bf19a6556
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
@@ -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'
|
|
37
|
-
spec.add_development_dependency 'rubocop-legion'
|
|
38
|
-
spec.add_development_dependency 'rubocop-rspec'
|
|
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
|
|
@@ -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
|
data/spec/legion/extensions/agentic/learning/outcome_listener/helpers/domain_extractor_spec.rb
ADDED
|
@@ -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
|
data/spec/legion/extensions/agentic/learning/outcome_listener/runners/outcome_listener_spec.rb
ADDED
|
@@ -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
|
+
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: '
|
|
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: '
|
|
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
|
|
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
|
|
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: '
|
|
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: '
|
|
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
|