lex-synapse 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -0
- data/CLAUDE.md +3 -3
- data/README.md +1 -1
- data/lib/legion/extensions/synapse/actors/challenge.rb +31 -0
- data/lib/legion/extensions/synapse/actors/homeostasis.rb +28 -14
- data/lib/legion/extensions/synapse/client.rb +21 -0
- data/lib/legion/extensions/synapse/data/migrations/005_add_synapse_challenges.rb +33 -0
- data/lib/legion/extensions/synapse/data/models/synapse_challenge.rb +26 -0
- data/lib/legion/extensions/synapse/helpers/challenge.rb +55 -0
- data/lib/legion/extensions/synapse/helpers/confidence.rb +3 -1
- data/lib/legion/extensions/synapse/helpers/proposals.rb +1 -1
- data/lib/legion/extensions/synapse/runners/challenge.rb +249 -0
- data/lib/legion/extensions/synapse/version.rb +1 -1
- metadata +6 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c62483ced1238be0da450c077108fa58541aa5964b27f3c6977ad11203202115
|
|
4
|
+
data.tar.gz: c8c8a52ff8eb60ebd971a4eeae4ae0a53709629548a58c4409bbdd7c5d3cfebe
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 50c4b74beca7a56c91953d34dd8179cd7f1c42af111ef05be4c9ec62f154135e93fac708c0270ed9c47ee73be987dd8386c5c47b638988982ff454980a4f2e0e
|
|
7
|
+
data.tar.gz: 3b6683e9c804a465f47d5fde8471768896ea7c4554e597a8a26c03313fb32e3d30c97cfa9e071726997455a3a5d4ea4392e1d9d94a20ce9ccc03c74de6318a2a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.0] - 2026-03-22
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- Adversarial challenge phase for proposals: conflict detection, LLM challenge, weighted aggregation
|
|
7
|
+
- `synapse_challenges` table (migration 005) for per-challenge verdict tracking
|
|
8
|
+
- `Runners::Challenge` with challenge_proposal, resolve_challenge_outcomes, run_challenge_cycle
|
|
9
|
+
- `Helpers::Challenge` with settings, constants, impact threshold helpers
|
|
10
|
+
- `Actors::Challenge` polling every 60s for pending proposals
|
|
11
|
+
- Challenger confidence tracking with outcome-based learning loop
|
|
12
|
+
- Auto-accept/auto-reject thresholds for unanimous verdicts
|
|
13
|
+
- Client methods: challenge_proposal, challenges, challenger_stats
|
|
14
|
+
- New proposal statuses: auto_accepted, auto_rejected
|
|
15
|
+
- Impact scoring gates LLM challenge (expensive calls only for high-impact proposals)
|
|
16
|
+
|
|
17
|
+
## [0.3.2] - 2026-03-21
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
- Homeostasis actor converted to self-contained pattern — was referencing non-existent `Runners::Homeostasis` and `check_homeostasis` method, now implements `action` directly using `Helpers::Homeostasis` spike/drought checks
|
|
21
|
+
|
|
22
|
+
## [0.3.1] - 2026-03-20
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- Emit `synapse.confidence_update` event on confidence adjustment for safety metrics
|
|
26
|
+
|
|
3
27
|
## [0.3.0] - 2026-03-19
|
|
4
28
|
|
|
5
29
|
### Added
|
data/CLAUDE.md
CHANGED
|
@@ -10,7 +10,7 @@ Cognitive routing layer that wraps task chain relationships with observation, le
|
|
|
10
10
|
|
|
11
11
|
**GitHub**: https://github.com/LegionIO/lex-synapse
|
|
12
12
|
**License**: MIT
|
|
13
|
-
**Version**: 0.3.
|
|
13
|
+
**Version**: 0.3.2
|
|
14
14
|
|
|
15
15
|
## Architecture
|
|
16
16
|
|
|
@@ -110,11 +110,11 @@ Legion::Extensions::Synapse
|
|
|
110
110
|
|
|
111
111
|
```bash
|
|
112
112
|
bundle install
|
|
113
|
-
bundle exec rspec #
|
|
113
|
+
bundle exec rspec # 353 specs, 0 failures
|
|
114
114
|
bundle exec rubocop # 0 offenses
|
|
115
115
|
```
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
353 specs, 95%+ coverage. Uses in-memory SQLite for model/runner tests.
|
|
118
118
|
|
|
119
119
|
---
|
|
120
120
|
|
data/README.md
CHANGED
|
@@ -146,7 +146,7 @@ Three tables: `synapses` (core routing definition + confidence + status), `synap
|
|
|
146
146
|
## Dependencies
|
|
147
147
|
|
|
148
148
|
- `lex-conditioner` >= 0.3.0
|
|
149
|
-
- `lex-transformer` >= 0.
|
|
149
|
+
- `lex-transformer` >= 0.3.0
|
|
150
150
|
- Ruby >= 3.4
|
|
151
151
|
- [LegionIO](https://github.com/LegionIO/LegionIO) framework (for AMQP actor mode)
|
|
152
152
|
- Standalone Client works without the framework (requires Sequel + database)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Synapse
|
|
6
|
+
module Actor
|
|
7
|
+
class Challenge < Legion::Extensions::Actors::Every
|
|
8
|
+
def runner_function
|
|
9
|
+
'run_challenge_cycle'
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def time
|
|
13
|
+
60
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def use_runner?
|
|
17
|
+
false
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def check_subtask?
|
|
21
|
+
false
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def generate_task?
|
|
25
|
+
false
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -5,24 +5,38 @@ module Legion
|
|
|
5
5
|
module Synapse
|
|
6
6
|
module Actor
|
|
7
7
|
class Homeostasis < Legion::Extensions::Actors::Every
|
|
8
|
-
def
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
def runner_class = self.class
|
|
9
|
+
def time = 30
|
|
10
|
+
def use_runner? = false
|
|
11
|
+
def check_subtask? = false
|
|
12
|
+
def generate_task? = false
|
|
11
13
|
|
|
12
|
-
def
|
|
13
|
-
|
|
14
|
-
end
|
|
14
|
+
def action(**_opts)
|
|
15
|
+
return { status: :skipped, reason: :no_data } unless defined?(Legion::Data)
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
end
|
|
17
|
+
results = { spikes: 0, droughts: 0, updated: 0 }
|
|
18
|
+
return results unless defined?(Legion::Extensions::Synapse::Data::Model::Synapse)
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
Legion::Extensions::Synapse::Data::Model::Synapse
|
|
21
|
+
.where(status: 'active')
|
|
22
|
+
.where { baseline_throughput > 0 } # rubocop:disable Style/NumericPredicate
|
|
23
|
+
.each do |synapse|
|
|
24
|
+
baseline = synapse.baseline_throughput
|
|
25
|
+
signals = synapse.signals_dataset.where { created_at > (Time.now - 60) }.count
|
|
26
|
+
current = signals.to_f
|
|
27
|
+
|
|
28
|
+
if Helpers::Homeostasis.spike?(current, baseline, duration_seconds: 60)
|
|
29
|
+
results[:spikes] += 1
|
|
30
|
+
elsif Helpers::Homeostasis.drought?(current, baseline, silent_seconds: 60)
|
|
31
|
+
results[:droughts] += 1
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
new_baseline = Helpers::Homeostasis.update_baseline(baseline, current)
|
|
35
|
+
synapse.update(baseline_throughput: new_baseline)
|
|
36
|
+
results[:updated] += 1
|
|
37
|
+
end
|
|
23
38
|
|
|
24
|
-
|
|
25
|
-
false
|
|
39
|
+
results
|
|
26
40
|
end
|
|
27
41
|
end
|
|
28
42
|
end
|
|
@@ -11,6 +11,7 @@ require_relative 'runners/dream'
|
|
|
11
11
|
require_relative 'runners/promote'
|
|
12
12
|
require_relative 'runners/retrieve'
|
|
13
13
|
require_relative 'runners/propose'
|
|
14
|
+
require_relative 'runners/challenge'
|
|
14
15
|
require_relative 'data/models/synapse_proposal'
|
|
15
16
|
require_relative 'helpers/proposals'
|
|
16
17
|
|
|
@@ -29,6 +30,7 @@ module Legion
|
|
|
29
30
|
include Runners::Promote
|
|
30
31
|
include Runners::Retrieve
|
|
31
32
|
include Runners::Propose
|
|
33
|
+
include Runners::Challenge
|
|
32
34
|
|
|
33
35
|
attr_reader :conditioner_client, :transformer_client
|
|
34
36
|
|
|
@@ -79,6 +81,25 @@ module Legion
|
|
|
79
81
|
proposal.update(status: status, reviewed_at: Time.now)
|
|
80
82
|
{ success: true, proposal_id: proposal_id, status: status }
|
|
81
83
|
end
|
|
84
|
+
|
|
85
|
+
def challenge_proposal(proposal_id:)
|
|
86
|
+
super(proposal_id: proposal_id, transformer_client: @transformer_client)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def challenges(proposal_id:)
|
|
90
|
+
Data::Model.define_synapse_challenge_model
|
|
91
|
+
Data::Model::SynapseChallenge.where(proposal_id: proposal_id).order(Sequel.desc(:id)).all
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def challenger_stats
|
|
95
|
+
Data::Model.define_synapse_challenge_model
|
|
96
|
+
resolved = Data::Model::SynapseChallenge.exclude(outcome: nil)
|
|
97
|
+
{
|
|
98
|
+
total: resolved.count,
|
|
99
|
+
correct: resolved.where(outcome: 'correct').count,
|
|
100
|
+
by_type: resolved.group_and_count(:challenger_type).to_h { |r| [r[:challenger_type], r[:count]] }
|
|
101
|
+
}
|
|
102
|
+
end
|
|
82
103
|
end
|
|
83
104
|
end
|
|
84
105
|
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
Sequel.migration do
|
|
4
|
+
up do
|
|
5
|
+
create_table(:synapse_challenges) do
|
|
6
|
+
primary_key :id
|
|
7
|
+
foreign_key :proposal_id, :synapse_proposals, null: false, index: true
|
|
8
|
+
String :challenger_type, null: false, size: 50
|
|
9
|
+
String :verdict, null: false, size: 50
|
|
10
|
+
String :reasoning, text: true
|
|
11
|
+
Float :challenger_confidence, default: 0.5
|
|
12
|
+
DateTime :created_at, null: false, default: Sequel::CURRENT_TIMESTAMP
|
|
13
|
+
DateTime :resolved_at
|
|
14
|
+
String :outcome, size: 50
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
alter_table(:synapse_proposals) do
|
|
18
|
+
add_column :challenge_state, String, size: 50
|
|
19
|
+
add_column :challenge_score, Float
|
|
20
|
+
add_column :impact_score, Float
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
down do
|
|
25
|
+
drop_table :synapse_challenges
|
|
26
|
+
|
|
27
|
+
alter_table(:synapse_proposals) do
|
|
28
|
+
drop_column :challenge_state
|
|
29
|
+
drop_column :challenge_score
|
|
30
|
+
drop_column :impact_score
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Synapse
|
|
6
|
+
module Data
|
|
7
|
+
module Model
|
|
8
|
+
def self.define_synapse_challenge_model
|
|
9
|
+
return if const_defined?(:SynapseChallenge, false)
|
|
10
|
+
return unless defined?(Legion::Data) && Legion::Settings.dig(:data, :connected)
|
|
11
|
+
|
|
12
|
+
db = Sequel::Model.db
|
|
13
|
+
return unless db&.table_exists?(:synapse_challenges)
|
|
14
|
+
|
|
15
|
+
klass = Class.new(Sequel::Model(:synapse_challenges)) do
|
|
16
|
+
many_to_one :proposal, class: 'Legion::Extensions::Synapse::Data::Model::SynapseProposal',
|
|
17
|
+
key: :proposal_id
|
|
18
|
+
end
|
|
19
|
+
klass.set_primary_key :id
|
|
20
|
+
const_set(:SynapseChallenge, klass)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Extensions
|
|
5
|
+
module Synapse
|
|
6
|
+
module Helpers
|
|
7
|
+
module Challenge
|
|
8
|
+
VALID_VERDICTS = %w[support challenge abstain].freeze
|
|
9
|
+
VALID_CHALLENGER_TYPES = %w[conflict llm].freeze
|
|
10
|
+
VALID_OUTCOMES = %w[correct incorrect].freeze
|
|
11
|
+
VALID_CHALLENGE_STATES = %w[challenging challenged].freeze
|
|
12
|
+
|
|
13
|
+
IMPACT_WEIGHTS = {
|
|
14
|
+
'llm_transform' => 0.7,
|
|
15
|
+
'transform_mutation' => 0.5,
|
|
16
|
+
'attention_mutation' => 0.6,
|
|
17
|
+
'route_change' => 0.8
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
DEFAULT_SETTINGS = {
|
|
21
|
+
enabled: true,
|
|
22
|
+
impact_threshold: 0.3,
|
|
23
|
+
auto_accept_threshold: 0.85,
|
|
24
|
+
auto_reject_threshold: 0.15,
|
|
25
|
+
llm_engine_options: { temperature: 0.2, max_tokens: 512 },
|
|
26
|
+
outcome_observation_window: 50,
|
|
27
|
+
max_per_cycle: 5,
|
|
28
|
+
challenger_starting_confidence: 0.5,
|
|
29
|
+
challenger_correct_adjustment: 0.05,
|
|
30
|
+
challenger_incorrect_adjustment: -0.08
|
|
31
|
+
}.freeze
|
|
32
|
+
|
|
33
|
+
class << self
|
|
34
|
+
def settings
|
|
35
|
+
raw = Legion::Settings.dig('lex-synapse', 'challenge')
|
|
36
|
+
return DEFAULT_SETTINGS.dup unless raw.is_a?(Hash)
|
|
37
|
+
|
|
38
|
+
merged = DEFAULT_SETTINGS.dup
|
|
39
|
+
raw.each { |k, v| merged[k.to_sym] = v unless v.nil? }
|
|
40
|
+
merged
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def enabled?
|
|
44
|
+
settings[:enabled] == true
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def above_impact_threshold?(impact_score)
|
|
48
|
+
impact_score >= settings[:impact_threshold]
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -36,7 +36,9 @@ module Legion
|
|
|
36
36
|
delta = ADJUSTMENTS.fetch(event, 0)
|
|
37
37
|
result = confidence + delta
|
|
38
38
|
result += ADJUSTMENTS[:consecutive_bonus] if event == :success && consecutive_successes > CONSECUTIVE_BONUS_THRESHOLD
|
|
39
|
-
clamp(result)
|
|
39
|
+
new_confidence = clamp(result)
|
|
40
|
+
Legion::Events.emit('synapse.confidence_update', delta: delta, event: event, new_confidence: new_confidence) if defined?(Legion::Events)
|
|
41
|
+
new_confidence
|
|
40
42
|
end
|
|
41
43
|
|
|
42
44
|
def decay(confidence, hours: 1)
|
|
@@ -7,7 +7,7 @@ module Legion
|
|
|
7
7
|
module Proposals
|
|
8
8
|
VALID_PROPOSAL_TYPES = %w[llm_transform attention_mutation transform_mutation route_change].freeze
|
|
9
9
|
VALID_TRIGGERS = %w[reactive proactive].freeze
|
|
10
|
-
VALID_STATUSES = %w[pending approved rejected applied expired].freeze
|
|
10
|
+
VALID_STATUSES = %w[pending challenging challenged approved rejected applied expired auto_accepted auto_rejected].freeze
|
|
11
11
|
|
|
12
12
|
DEFAULT_SETTINGS = {
|
|
13
13
|
enabled: true,
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../helpers/challenge'
|
|
4
|
+
require_relative '../helpers/confidence'
|
|
5
|
+
require_relative '../data/models/synapse'
|
|
6
|
+
require_relative '../data/models/synapse_proposal'
|
|
7
|
+
require_relative '../data/models/synapse_challenge'
|
|
8
|
+
require_relative '../data/models/synapse_signal'
|
|
9
|
+
|
|
10
|
+
module Legion
|
|
11
|
+
module Extensions
|
|
12
|
+
module Synapse
|
|
13
|
+
module Runners
|
|
14
|
+
module Challenge
|
|
15
|
+
def pending_challenges
|
|
16
|
+
Data::Model.define_synapse_proposal_model
|
|
17
|
+
Data::Model::SynapseProposal.where(status: 'pending', challenge_state: nil)
|
|
18
|
+
.order(Sequel.asc(:id)).all
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def challenge_proposal(proposal_id:, transformer_client: nil)
|
|
22
|
+
Data::Model.define_synapse_proposal_model
|
|
23
|
+
Data::Model.define_synapse_challenge_model
|
|
24
|
+
Data::Model.define_synapse_model
|
|
25
|
+
Data::Model.define_synapse_signal_model
|
|
26
|
+
|
|
27
|
+
return { success: true, skipped: true } unless Helpers::Challenge.enabled?
|
|
28
|
+
|
|
29
|
+
proposal = Data::Model::SynapseProposal[proposal_id]
|
|
30
|
+
return { success: false, error: 'proposal not found' } unless proposal
|
|
31
|
+
return { success: false, error: 'proposal not challengeable' } unless proposal.status == 'pending' && proposal.challenge_state.nil?
|
|
32
|
+
|
|
33
|
+
synapse = Data::Model::Synapse[proposal.synapse_id]
|
|
34
|
+
return { success: false, error: 'synapse not found' } unless synapse
|
|
35
|
+
|
|
36
|
+
proposal.update(challenge_state: 'challenging')
|
|
37
|
+
|
|
38
|
+
impact = calculate_impact_score(proposal, synapse)
|
|
39
|
+
proposal.update(impact_score: impact)
|
|
40
|
+
|
|
41
|
+
conflict_check(proposal)
|
|
42
|
+
|
|
43
|
+
llm_challenge(proposal, synapse, transformer_client) if Helpers::Challenge.above_impact_threshold?(impact) && transformer_client
|
|
44
|
+
|
|
45
|
+
aggregate_challenges(proposal)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def resolve_challenge_outcomes(proposal_id:)
|
|
49
|
+
Data::Model.define_synapse_proposal_model
|
|
50
|
+
Data::Model.define_synapse_challenge_model
|
|
51
|
+
Data::Model.define_synapse_signal_model
|
|
52
|
+
|
|
53
|
+
proposal = Data::Model::SynapseProposal[proposal_id]
|
|
54
|
+
return { success: false, error: 'proposal not found' } unless proposal
|
|
55
|
+
return { success: false, error: 'proposal not applied' } unless proposal.status == 'applied'
|
|
56
|
+
|
|
57
|
+
settings = Helpers::Challenge.settings
|
|
58
|
+
window = settings[:outcome_observation_window] || 50
|
|
59
|
+
|
|
60
|
+
signals = Data::Model::SynapseSignal.where(synapse_id: proposal.synapse_id)
|
|
61
|
+
.order(Sequel.desc(:id)).limit(window).all
|
|
62
|
+
return { success: false, error: 'insufficient signals' } if signals.size < window
|
|
63
|
+
|
|
64
|
+
success_rate = signals.count(&:transform_success).to_f / signals.size
|
|
65
|
+
proposal_succeeded = success_rate >= 0.7
|
|
66
|
+
|
|
67
|
+
challenges = Data::Model::SynapseChallenge.where(proposal_id: proposal_id)
|
|
68
|
+
.exclude(verdict: 'abstain').all
|
|
69
|
+
|
|
70
|
+
challenges.each do |challenge|
|
|
71
|
+
supported = challenge.verdict == 'support'
|
|
72
|
+
correct = (supported && proposal_succeeded) || (!supported && !proposal_succeeded)
|
|
73
|
+
outcome = correct ? 'correct' : 'incorrect'
|
|
74
|
+
|
|
75
|
+
adj = correct ? settings[:challenger_correct_adjustment] : settings[:challenger_incorrect_adjustment]
|
|
76
|
+
new_conf = (challenge.challenger_confidence + adj).clamp(0.0, 1.0)
|
|
77
|
+
|
|
78
|
+
challenge.update(outcome: outcome, challenger_confidence: new_conf, resolved_at: Time.now)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
{ success: true, proposal_id: proposal_id, success_rate: success_rate, resolved: challenges.size }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def run_challenge_cycle(transformer_client: nil)
|
|
85
|
+
Data::Model.define_synapse_proposal_model
|
|
86
|
+
Data::Model.define_synapse_challenge_model
|
|
87
|
+
return { challenged: 0, resolved: 0 } unless Helpers::Challenge.enabled?
|
|
88
|
+
|
|
89
|
+
settings = Helpers::Challenge.settings
|
|
90
|
+
max = settings[:max_per_cycle] || 5
|
|
91
|
+
|
|
92
|
+
challenged = 0
|
|
93
|
+
pending_challenges.first(max).each do |proposal|
|
|
94
|
+
challenge_proposal(proposal_id: proposal.id, transformer_client: transformer_client)
|
|
95
|
+
challenged += 1
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
resolved = 0
|
|
99
|
+
window = settings[:outcome_observation_window] || 50
|
|
100
|
+
Data::Model.define_synapse_signal_model
|
|
101
|
+
Data::Model::SynapseProposal.where(status: 'applied').each do |proposal|
|
|
102
|
+
cutoff = proposal.respond_to?(:reviewed_at) && proposal.reviewed_at ? proposal.reviewed_at : proposal.created_at
|
|
103
|
+
post_signals = Data::Model::SynapseSignal.where(synapse_id: proposal.synapse_id)
|
|
104
|
+
.where { created_at >= cutoff }.count
|
|
105
|
+
next unless post_signals >= window
|
|
106
|
+
|
|
107
|
+
unresolved = Data::Model::SynapseChallenge.where(proposal_id: proposal.id, outcome: nil)
|
|
108
|
+
.exclude(verdict: 'abstain')
|
|
109
|
+
next unless unresolved.any?
|
|
110
|
+
|
|
111
|
+
resolve_challenge_outcomes(proposal_id: proposal.id)
|
|
112
|
+
resolved += 1
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
{ challenged: challenged, resolved: resolved }
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
private
|
|
119
|
+
|
|
120
|
+
def conflict_check(proposal)
|
|
121
|
+
conflicts = Data::Model::SynapseProposal.where(
|
|
122
|
+
synapse_id: proposal.synapse_id,
|
|
123
|
+
proposal_type: proposal.proposal_type,
|
|
124
|
+
status: 'pending'
|
|
125
|
+
).exclude(id: proposal.id)
|
|
126
|
+
|
|
127
|
+
if conflicts.any?
|
|
128
|
+
Data::Model::SynapseChallenge.create(
|
|
129
|
+
proposal_id: proposal.id, challenger_type: 'conflict',
|
|
130
|
+
verdict: 'challenge',
|
|
131
|
+
reasoning: "#{conflicts.count} conflicting #{proposal.proposal_type} proposal(s) pending on same synapse",
|
|
132
|
+
challenger_confidence: Helpers::Challenge.settings[:challenger_starting_confidence]
|
|
133
|
+
)
|
|
134
|
+
else
|
|
135
|
+
Data::Model::SynapseChallenge.create(
|
|
136
|
+
proposal_id: proposal.id, challenger_type: 'conflict',
|
|
137
|
+
verdict: 'support', reasoning: 'no conflicting proposals',
|
|
138
|
+
challenger_confidence: Helpers::Challenge.settings[:challenger_starting_confidence]
|
|
139
|
+
)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def llm_challenge(proposal, synapse, transformer_client)
|
|
144
|
+
prompt = build_challenge_prompt(proposal, synapse)
|
|
145
|
+
engine_options = Helpers::Challenge.settings[:llm_engine_options]
|
|
146
|
+
|
|
147
|
+
result = transformer_client.transform(
|
|
148
|
+
transformation: prompt, payload: {}, engine: :llm, engine_options: engine_options
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
verdict, reasoning = parse_llm_verdict(result[:success] ? result[:result] : nil)
|
|
152
|
+
|
|
153
|
+
llm_confidence = rolling_llm_confidence
|
|
154
|
+
Data::Model::SynapseChallenge.create(
|
|
155
|
+
proposal_id: proposal.id, challenger_type: 'llm',
|
|
156
|
+
verdict: verdict, reasoning: reasoning,
|
|
157
|
+
challenger_confidence: llm_confidence
|
|
158
|
+
)
|
|
159
|
+
rescue StandardError => e
|
|
160
|
+
Legion::Logging.warn("Challenge LLM call failed: #{e.message}") if defined?(Legion::Logging)
|
|
161
|
+
Data::Model::SynapseChallenge.create(
|
|
162
|
+
proposal_id: proposal.id, challenger_type: 'llm',
|
|
163
|
+
verdict: 'abstain', reasoning: "LLM error: #{e.message}",
|
|
164
|
+
challenger_confidence: Helpers::Challenge.settings[:challenger_starting_confidence]
|
|
165
|
+
)
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def aggregate_challenges(proposal)
|
|
169
|
+
challenges = Data::Model::SynapseChallenge.where(proposal_id: proposal.id)
|
|
170
|
+
.exclude(verdict: 'abstain').all
|
|
171
|
+
|
|
172
|
+
if challenges.empty?
|
|
173
|
+
proposal.update(challenge_state: 'challenged', challenge_score: 0.5)
|
|
174
|
+
return { success: true, challenge_score: 0.5, decision: 'challenged' }
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
support_weight = challenges.select { |c| c.verdict == 'support' }.sum(&:challenger_confidence)
|
|
178
|
+
challenge_weight = challenges.select { |c| c.verdict == 'challenge' }.sum(&:challenger_confidence)
|
|
179
|
+
total = support_weight + challenge_weight
|
|
180
|
+
|
|
181
|
+
score = total.zero? ? 0.5 : support_weight / total
|
|
182
|
+
|
|
183
|
+
settings = Helpers::Challenge.settings
|
|
184
|
+
decision = if score >= settings[:auto_accept_threshold]
|
|
185
|
+
proposal.update(status: 'auto_accepted', challenge_state: 'challenged', challenge_score: score)
|
|
186
|
+
'auto_accepted'
|
|
187
|
+
elsif score <= settings[:auto_reject_threshold]
|
|
188
|
+
proposal.update(status: 'auto_rejected', challenge_state: 'challenged', challenge_score: score)
|
|
189
|
+
'auto_rejected'
|
|
190
|
+
else
|
|
191
|
+
proposal.update(challenge_state: 'challenged', challenge_score: score)
|
|
192
|
+
'challenged'
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
{ success: true, challenge_score: score, decision: decision }
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def calculate_impact_score(proposal, synapse)
|
|
199
|
+
base = Helpers::Challenge::IMPACT_WEIGHTS.fetch(proposal.proposal_type, 0.5)
|
|
200
|
+
recent_signals = Data::Model::SynapseSignal.where(synapse_id: synapse.id).count
|
|
201
|
+
baseline = [synapse.respond_to?(:baseline_throughput) && synapse.baseline_throughput ? synapse.baseline_throughput : 1.0, 1.0].max
|
|
202
|
+
throughput_factor = [recent_signals.to_f / baseline, 2.0].min
|
|
203
|
+
|
|
204
|
+
(base * synapse.confidence * throughput_factor).clamp(0.0, 1.0)
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def build_challenge_prompt(proposal, synapse)
|
|
208
|
+
"Evaluate this proposed change to a cognitive routing synapse.\n\n" \
|
|
209
|
+
"Synapse confidence: #{synapse.confidence}\n" \
|
|
210
|
+
"Proposal type: #{proposal.proposal_type}\n" \
|
|
211
|
+
"Rationale: #{proposal.rationale}\n" \
|
|
212
|
+
"Proposed inputs: #{proposal.inputs}\n" \
|
|
213
|
+
"Proposed output: #{proposal.output}\n\n" \
|
|
214
|
+
"Is this change sound? Respond in exactly this format:\n" \
|
|
215
|
+
"VERDICT: SUPPORT or CHALLENGE or ABSTAIN\n" \
|
|
216
|
+
'REASONING: one sentence explanation'
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def parse_llm_verdict(response)
|
|
220
|
+
return ['abstain', 'no LLM response'] unless response.is_a?(String)
|
|
221
|
+
|
|
222
|
+
text = response.to_s.strip
|
|
223
|
+
verdict = if text.match?(/VERDICT:\s*SUPPORT/i)
|
|
224
|
+
'support'
|
|
225
|
+
elsif text.match?(/VERDICT:\s*CHALLENGE/i)
|
|
226
|
+
'challenge'
|
|
227
|
+
else
|
|
228
|
+
'abstain'
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
reasoning_match = text.match(/REASONING:\s*(.+)/i)
|
|
232
|
+
reasoning = reasoning_match ? reasoning_match[1].strip : text.slice(0, 200)
|
|
233
|
+
|
|
234
|
+
[verdict, reasoning]
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def rolling_llm_confidence
|
|
238
|
+
recent = Data::Model::SynapseChallenge.where(challenger_type: 'llm')
|
|
239
|
+
.exclude(outcome: nil)
|
|
240
|
+
.order(Sequel.desc(:id)).limit(20).all
|
|
241
|
+
return Helpers::Challenge.settings[:challenger_starting_confidence] if recent.empty?
|
|
242
|
+
|
|
243
|
+
recent.first.challenger_confidence
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: lex-synapse
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -56,6 +56,7 @@ files:
|
|
|
56
56
|
- Rakefile
|
|
57
57
|
- lex-synapse.gemspec
|
|
58
58
|
- lib/legion/extensions/synapse.rb
|
|
59
|
+
- lib/legion/extensions/synapse/actors/challenge.rb
|
|
59
60
|
- lib/legion/extensions/synapse/actors/crystallize.rb
|
|
60
61
|
- lib/legion/extensions/synapse/actors/decay.rb
|
|
61
62
|
- lib/legion/extensions/synapse/actors/evaluate.rb
|
|
@@ -67,14 +68,18 @@ files:
|
|
|
67
68
|
- lib/legion/extensions/synapse/data/migrations/002_create_synapse_mutations.rb
|
|
68
69
|
- lib/legion/extensions/synapse/data/migrations/003_create_synapse_signals.rb
|
|
69
70
|
- lib/legion/extensions/synapse/data/migrations/004_create_synapse_proposals.rb
|
|
71
|
+
- lib/legion/extensions/synapse/data/migrations/005_add_synapse_challenges.rb
|
|
70
72
|
- lib/legion/extensions/synapse/data/models/synapse.rb
|
|
73
|
+
- lib/legion/extensions/synapse/data/models/synapse_challenge.rb
|
|
71
74
|
- lib/legion/extensions/synapse/data/models/synapse_mutation.rb
|
|
72
75
|
- lib/legion/extensions/synapse/data/models/synapse_proposal.rb
|
|
73
76
|
- lib/legion/extensions/synapse/data/models/synapse_signal.rb
|
|
77
|
+
- lib/legion/extensions/synapse/helpers/challenge.rb
|
|
74
78
|
- lib/legion/extensions/synapse/helpers/confidence.rb
|
|
75
79
|
- lib/legion/extensions/synapse/helpers/homeostasis.rb
|
|
76
80
|
- lib/legion/extensions/synapse/helpers/proposals.rb
|
|
77
81
|
- lib/legion/extensions/synapse/helpers/relationship_wrapper.rb
|
|
82
|
+
- lib/legion/extensions/synapse/runners/challenge.rb
|
|
78
83
|
- lib/legion/extensions/synapse/runners/crystallize.rb
|
|
79
84
|
- lib/legion/extensions/synapse/runners/dream.rb
|
|
80
85
|
- lib/legion/extensions/synapse/runners/evaluate.rb
|