lex-synapse 0.2.2 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c1961edd72029f290abdb673e1a4b23736cdc52b963d11243e4f47e4f38c65f1
4
- data.tar.gz: 83be98e416e226e960e331fa24c197f99ff236cc419ea5afd26b050ee6e09fcd
3
+ metadata.gz: 60a95b064e889f6594f48ea66c0c12e8360b4679ef9daf572e31560e304ea277
4
+ data.tar.gz: a96ea18996d7b3d87ee309cbba68f2258b34f40fa9a0a61068562f81f347177d
5
5
  SHA512:
6
- metadata.gz: ef8000741910d6a3532cafb21a4c82924e17067359609524d41df993947677bc7fe6f505f3230cbdc94a89cca0c6a8e3db7a026a77aed2a4edb5f1a43030c227
7
- data.tar.gz: 86fccd6c3ecb7f4a43c0a5030c21a82af341ea50fc7e0b4e334ef77718c637358018a5fca556361ab2a26c2870879db1d54eb91505a947b9fbf3779aa05f16a4
6
+ metadata.gz: 9faa202ea887493c0ab6b3b9df9323757ca00e1fca347b0669876241ef4733aa4f333c5153c8b0a46d59fd28d6ba5e5286f5e7d541dc47d21e65ea4a9b6f554a
7
+ data.tar.gz: 85411a3ec047e7d9e944b61d08c03ab3ee3868f58887b6e13c3e6ab46d1fcad3ef5a56ddf73d12cc8a32a77b4d1c2d34386caa9d2711950c464a818229fdb495
data/CHANGELOG.md CHANGED
@@ -1,5 +1,33 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.0] - 2026-03-19
4
+
5
+ ### Added
6
+ - Autonomous observation mode: proposal engine for AUTONOMOUS tier (confidence 0.8+)
7
+ - `synapse_proposals` table with migration 004
8
+ - `Runners::Propose` with reactive proposals (no-template, transform failure, pain correlation)
9
+ - `Runners::Propose` with proactive analysis (success rate degradation, payload drift)
10
+ - `Actors::Propose` periodic actor for proactive analysis (every 300s)
11
+ - `Helpers::Proposals` settings helper with configurable thresholds
12
+ - Proposal hook in `Runners::Evaluate` for autonomous synapses (gated by settings)
13
+ - Client `proposals(synapse_id:, status:)` query method
14
+ - Client `review_proposal(proposal_id:, status:)` for approving/rejecting proposals
15
+ - LLM-backed proposal generation via lex-transformer LLM engine
16
+ - Proposal deduplication within configurable window
17
+ - Integration specs for full proposal workflow
18
+ - Settings: `lex-synapse.proposals.*` for master switch, reactive/proactive toggles, max_per_run, LLM engine options, thresholds
19
+
20
+ ### Changed
21
+ - `lex-transformer` dependency bumped to >= 0.3.0 (requires LLM engine with engine_options)
22
+
23
+ ## [0.2.3] - 2026-03-19
24
+
25
+ ### Fixed
26
+ - Guard synapse model definition against missing table at require time; replace eager `class Synapse < Sequel::Model(:synapses)` with lazy `define_synapse_model` module method that checks `Legion::Data` connected and `table_exists?` before defining the constant — prevents `PG::UndefinedTable` error when gem loads before migrations run
27
+ - Apply same lazy-define guard to `SynapseMutation` and `SynapseSignal` models
28
+ - Add explicit `set_primary_key :id` and `key:` options on associations in anonymous Sequel model classes to prevent Sequel inferring `_id` column name for unnamed classes
29
+ - Call `define_synapse_model` (and related) at the top of each runner method and `RelationshipWrapper` class method before first model reference
30
+
3
31
  ## [0.2.2] - 2026-03-18
4
32
 
5
33
  ### Fixed
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.2.2
13
+ **Version**: 0.3.0
14
14
 
15
15
  ## Architecture
16
16
 
@@ -21,14 +21,20 @@ Legion::Extensions::Synapse
21
21
  │ ├── Pain # Subscription — task.failed handler
22
22
  │ ├── Crystallize # Every 300s — emergent synapse detection
23
23
  │ ├── Homeostasis # Every 30s — spike/drought monitoring
24
- └── Decay # Every 3600s — idle confidence decay
24
+ ├── Decay # Every 3600s — idle confidence decay
25
+ │ └── Propose # Every 300s — proactive proposal analysis for AUTONOMOUS synapses
25
26
  ├── Runners/
26
27
  │ ├── Evaluate # attention -> transform -> route -> record
27
28
  │ ├── Pain # failure recording, confidence hit, auto-revert
28
29
  │ ├── Crystallize # unrouted traffic analysis, emergent creation
29
30
  │ ├── Mutate # versioned self-modification with snapshots
30
31
  │ ├── Revert # rollback to previous mutation version
31
- └── Report # aggregate stats for GAIA consumption
32
+ ├── Report # aggregate stats for GAIA consumption
33
+ │ ├── Dream # replay historical signals in simulation mode; replay/simulate
34
+ │ ├── GaiaReport # GAIA tick hook: report confidence and health per synapse
35
+ │ ├── Promote # Apollo integration: promote high-confidence synapse patterns to shared knowledge
36
+ │ ├── Retrieve # Apollo integration: retrieve relevant synapse patterns from shared knowledge
37
+ │ └── Propose # reactive (signal-driven) + proactive (periodic) proposal generation
32
38
  ├── Helpers/
33
39
  │ ├── Confidence # scoring, adjustments, autonomy ranges, decay
34
40
  │ ├── Homeostasis # spike/drought detection, baseline tracking
@@ -75,23 +81,40 @@ Legion::Extensions::Synapse
75
81
  - **synapse_mutations**: Versioned change history with before/after JSON snapshots
76
82
  - **synapse_signals**: Per-signal outcome records (attention pass, transform success, latency, downstream outcome)
77
83
 
84
+ ## Autonomous Observation Mode (v0.3.0)
85
+
86
+ - **Proposal engine**: AUTONOMOUS tier (confidence 0.8+) generates proposals instead of executing autonomous actions
87
+ - **Reactive proposals**: on signal evaluation — no-template inference, transform failure fix, attention pain correlation
88
+ - **Proactive proposals**: periodic analysis — success rate degradation, payload drift detection
89
+ - **LLM-backed**: proposals call lex-transformer LLM engine for real output generation
90
+ - **Settings**: `lex-synapse.proposals.*` — enabled, reactive, proactive, max_per_run, llm_engine_options, thresholds
91
+ - **Data**: `synapse_proposals` table with status lifecycle (pending -> approved/rejected/applied/expired)
92
+ - **Client methods**: `proposals(synapse_id:, status:)`, `review_proposal(proposal_id:, status:)`
93
+
94
+ ## GAIA / Apollo Integration (v0.2.2)
95
+
96
+ - **GaiaReport runner**: Called during the GAIA tick cycle to report per-synapse confidence and health metrics.
97
+ - **Dream runner**: Replays historical signals in simulation mode. Used by the dream cycle to test routing hypothesis changes without affecting live state.
98
+ - **Promote runner**: Publishes high-confidence synapse patterns to the Apollo shared knowledge store when confidence exceeds threshold.
99
+ - **Retrieve runner**: Pulls relevant synapse patterns from Apollo to seed new synapses or adjust confidence for cold-start scenarios.
100
+
78
101
  ## Dependencies
79
102
 
80
103
  | Gem | Purpose |
81
104
  |-----|---------|
82
105
  | `lex-conditioner` >= 0.3.0 | Attention evaluation (condition rules) |
83
- | `lex-transformer` >= 0.2.0 | Payload transformation (template engines) |
106
+ | `lex-transformer` >= 0.3.0 | Payload transformation (template engines) |
84
107
  | `legion-data` | Required — database persistence via Sequel |
85
108
 
86
109
  ## Testing
87
110
 
88
111
  ```bash
89
112
  bundle install
90
- bundle exec rspec
91
- bundle exec rubocop
113
+ bundle exec rspec # 366 specs, 0 failures
114
+ bundle exec rubocop # 0 offenses
92
115
  ```
93
116
 
94
- 308 specs, 96%+ coverage. Uses in-memory SQLite for model/runner tests.
117
+ 366 specs, 95%+ coverage. Uses in-memory SQLite for model/runner tests.
95
118
 
96
119
  ---
97
120
 
data/lex-synapse.gemspec CHANGED
@@ -27,5 +27,5 @@ Gem::Specification.new do |spec|
27
27
  spec.require_paths = ['lib']
28
28
 
29
29
  spec.add_dependency 'lex-conditioner', '>= 0.3.0'
30
- spec.add_dependency 'lex-transformer', '>= 0.2.0'
30
+ spec.add_dependency 'lex-transformer', '>= 0.3.0'
31
31
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Synapse
6
+ module Actor
7
+ class Propose < Legion::Extensions::Actors::Every
8
+ def runner_function
9
+ 'propose_proactive'
10
+ end
11
+
12
+ def time
13
+ 300
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
@@ -10,6 +10,9 @@ require_relative 'runners/gaia_report'
10
10
  require_relative 'runners/dream'
11
11
  require_relative 'runners/promote'
12
12
  require_relative 'runners/retrieve'
13
+ require_relative 'runners/propose'
14
+ require_relative 'data/models/synapse_proposal'
15
+ require_relative 'helpers/proposals'
13
16
 
14
17
  module Legion
15
18
  module Extensions
@@ -25,6 +28,7 @@ module Legion
25
28
  include Runners::Dream
26
29
  include Runners::Promote
27
30
  include Runners::Retrieve
31
+ include Runners::Propose
28
32
 
29
33
  attr_reader :conditioner_client, :transformer_client
30
34
 
@@ -44,6 +48,7 @@ module Legion
44
48
 
45
49
  def create(source_function_id:, target_function_id:, attention: nil, transform: nil,
46
50
  routing_strategy: 'direct', origin: 'explicit', relationship_id: nil)
51
+ Data::Model.define_synapse_model
47
52
  Data::Model::Synapse.create(
48
53
  source_function_id: source_function_id,
49
54
  target_function_id: target_function_id,
@@ -56,6 +61,24 @@ module Legion
56
61
  status: origin == 'emergent' ? 'observing' : 'active'
57
62
  )
58
63
  end
64
+
65
+ def proposals(synapse_id:, status: nil)
66
+ Data::Model.define_synapse_proposal_model
67
+ dataset = Data::Model::SynapseProposal.where(synapse_id: synapse_id)
68
+ dataset = dataset.where(status: status) if status
69
+ dataset.order(Sequel.desc(:id)).all
70
+ end
71
+
72
+ def review_proposal(proposal_id:, status:)
73
+ Data::Model.define_synapse_proposal_model
74
+ return { success: false, error: "invalid status: #{status}" } unless Helpers::Proposals::VALID_STATUSES.include?(status)
75
+
76
+ proposal = Data::Model::SynapseProposal[proposal_id]
77
+ return { success: false, error: 'proposal not found' } unless proposal
78
+
79
+ proposal.update(status: status, reviewed_at: Time.now)
80
+ { success: true, proposal_id: proposal_id, status: status }
81
+ end
59
82
  end
60
83
  end
61
84
  end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sequel.migration do
4
+ up do
5
+ create_table(:synapse_proposals) do
6
+ primary_key :id
7
+ foreign_key :synapse_id, :synapses, null: false, index: true
8
+ Integer :signal_id
9
+ String :proposal_type, null: false, size: 50
10
+ String :trigger, null: false, size: 50
11
+ String :inputs, text: true
12
+ String :output, text: true
13
+ String :rationale, text: true
14
+ String :status, default: 'pending', size: 50
15
+ Float :estimated_confidence_impact
16
+ DateTime :created_at, null: false, default: Sequel::CURRENT_TIMESTAMP
17
+ DateTime :reviewed_at
18
+ end
19
+ end
20
+
21
+ down do
22
+ drop_table :synapse_proposals
23
+ end
24
+ end
@@ -5,9 +5,23 @@ module Legion
5
5
  module Synapse
6
6
  module Data
7
7
  module Model
8
- class Synapse < Sequel::Model(:synapses)
9
- one_to_many :mutations, class: 'Legion::Extensions::Synapse::Data::Model::SynapseMutation'
10
- one_to_many :signals, class: 'Legion::Extensions::Synapse::Data::Model::SynapseSignal'
8
+ def self.define_synapse_model
9
+ return if const_defined?(:Synapse, false)
10
+ return unless defined?(Legion::Data) && Legion::Settings.dig(:data, :connected)
11
+
12
+ db = Sequel::Model.db
13
+ return unless db&.table_exists?(:synapses)
14
+
15
+ klass = Class.new(Sequel::Model(:synapses)) do
16
+ one_to_many :mutations, class: 'Legion::Extensions::Synapse::Data::Model::SynapseMutation',
17
+ key: :synapse_id
18
+ one_to_many :signals, class: 'Legion::Extensions::Synapse::Data::Model::SynapseSignal',
19
+ key: :synapse_id
20
+ one_to_many :proposals, class: 'Legion::Extensions::Synapse::Data::Model::SynapseProposal',
21
+ key: :synapse_id
22
+ end
23
+ klass.set_primary_key :id
24
+ const_set(:Synapse, klass)
11
25
  end
12
26
  end
13
27
  end
@@ -5,8 +5,19 @@ module Legion
5
5
  module Synapse
6
6
  module Data
7
7
  module Model
8
- class SynapseMutation < Sequel::Model(:synapse_mutations)
9
- many_to_one :synapse, class: 'Legion::Extensions::Synapse::Data::Model::Synapse'
8
+ def self.define_synapse_mutation_model
9
+ return if const_defined?(:SynapseMutation, 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_mutations)
14
+
15
+ klass = Class.new(Sequel::Model(:synapse_mutations)) do
16
+ many_to_one :synapse, class: 'Legion::Extensions::Synapse::Data::Model::Synapse',
17
+ key: :synapse_id
18
+ end
19
+ klass.set_primary_key :id
20
+ const_set(:SynapseMutation, klass)
10
21
  end
11
22
  end
12
23
  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_proposal_model
9
+ return if const_defined?(:SynapseProposal, 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_proposals)
14
+
15
+ klass = Class.new(Sequel::Model(:synapse_proposals)) do
16
+ many_to_one :synapse, class: 'Legion::Extensions::Synapse::Data::Model::Synapse',
17
+ key: :synapse_id
18
+ end
19
+ klass.set_primary_key :id
20
+ const_set(:SynapseProposal, klass)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -5,8 +5,19 @@ module Legion
5
5
  module Synapse
6
6
  module Data
7
7
  module Model
8
- class SynapseSignal < Sequel::Model(:synapse_signals)
9
- many_to_one :synapse, class: 'Legion::Extensions::Synapse::Data::Model::Synapse'
8
+ def self.define_synapse_signal_model
9
+ return if const_defined?(:SynapseSignal, 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_signals)
14
+
15
+ klass = Class.new(Sequel::Model(:synapse_signals)) do
16
+ many_to_one :synapse, class: 'Legion::Extensions::Synapse::Data::Model::Synapse',
17
+ key: :synapse_id
18
+ end
19
+ klass.set_primary_key :id
20
+ const_set(:SynapseSignal, klass)
10
21
  end
11
22
  end
12
23
  end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Legion
4
+ module Extensions
5
+ module Synapse
6
+ module Helpers
7
+ module Proposals
8
+ VALID_PROPOSAL_TYPES = %w[llm_transform attention_mutation transform_mutation route_change].freeze
9
+ VALID_TRIGGERS = %w[reactive proactive].freeze
10
+ VALID_STATUSES = %w[pending approved rejected applied expired].freeze
11
+
12
+ DEFAULT_SETTINGS = {
13
+ enabled: true,
14
+ reactive: true,
15
+ proactive: true,
16
+ proactive_interval: 300,
17
+ max_per_run: 3,
18
+ llm_engine_options: { temperature: 0.3, max_tokens: 1024 },
19
+ success_rate_threshold: 0.8,
20
+ payload_drift_threshold: 0.2,
21
+ dedup_window_hours: 24
22
+ }.freeze
23
+
24
+ class << self
25
+ def settings
26
+ raw = Legion::Settings.dig('lex-synapse', 'proposals')
27
+ return DEFAULT_SETTINGS.dup unless raw.is_a?(Hash)
28
+
29
+ merged = DEFAULT_SETTINGS.dup
30
+ raw.each { |k, v| merged[k.to_sym] = v unless v.nil? }
31
+ merged
32
+ end
33
+
34
+ def enabled?
35
+ settings[:enabled] == true
36
+ end
37
+
38
+ def reactive?
39
+ s = settings
40
+ s[:enabled] == true && s[:reactive] == true
41
+ end
42
+
43
+ def proactive?
44
+ s = settings
45
+ s[:enabled] == true && s[:proactive] == true
46
+ end
47
+
48
+ def llm_engine_options
49
+ s = settings
50
+ opts = s[:llm_engine_options]
51
+ opts.is_a?(Hash) ? opts.transform_keys(&:to_sym) : DEFAULT_SETTINGS[:llm_engine_options].dup
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -10,6 +10,7 @@ module Legion
10
10
  module RelationshipWrapper
11
11
  class << self
12
12
  def wrap(relationship)
13
+ Data::Model.define_synapse_model
13
14
  existing = Data::Model::Synapse.where(relationship_id: relationship[:id]).first
14
15
  return existing if existing
15
16
 
@@ -27,6 +28,7 @@ module Legion
27
28
  end
28
29
 
29
30
  def unwrap(synapse_id)
31
+ Data::Model.define_synapse_model
30
32
  synapse = Data::Model::Synapse[synapse_id]
31
33
  return { success: false, error: 'synapse not found' } unless synapse
32
34
  return { success: false, error: 'not a wrapped relationship' } unless synapse.relationship_id
@@ -11,6 +11,7 @@ module Legion
11
11
  EMERGENCE_THRESHOLD = 20
12
12
 
13
13
  def crystallize(signal_pairs: [], threshold: EMERGENCE_THRESHOLD)
14
+ Data::Model.define_synapse_model
14
15
  created = []
15
16
 
16
17
  signal_pairs.each do |pair|
@@ -10,6 +10,8 @@ module Legion
10
10
  module Runners
11
11
  module Dream
12
12
  def dream_replay(synapse_id: nil, **)
13
+ Data::Model.define_synapse_model
14
+ Data::Model.define_synapse_mutation_model
13
15
  synapses = if synapse_id
14
16
  s = Data::Model::Synapse[synapse_id]
15
17
  s ? [s] : []
@@ -27,6 +29,7 @@ module Legion
27
29
  end
28
30
 
29
31
  def dream_simulate(synapse_id:, mutation_type:, changes:, **)
32
+ Data::Model.define_synapse_model
30
33
  synapse = Data::Model::Synapse[synapse_id]
31
34
  return { success: false, error: 'synapse not found' } unless synapse
32
35
 
@@ -1,15 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../helpers/confidence'
4
+ require_relative '../helpers/proposals'
4
5
  require_relative '../data/models/synapse'
6
+ require_relative '../data/models/synapse_mutation'
5
7
  require_relative '../data/models/synapse_signal'
8
+ require_relative 'propose'
6
9
 
7
10
  module Legion
8
11
  module Extensions
9
12
  module Synapse
10
13
  module Runners
11
14
  module Evaluate
15
+ include Propose
16
+
12
17
  def evaluate(synapse_id:, payload: {}, conditioner_client: nil, transformer_client: nil)
18
+ Data::Model.define_synapse_model
19
+ Data::Model.define_synapse_signal_model
20
+ Data::Model.define_synapse_proposal_model
13
21
  synapse = Data::Model::Synapse[synapse_id]
14
22
  return { success: false, error: 'synapse not found' } unless synapse
15
23
  return { success: false, error: 'synapse not active' } unless Helpers::Confidence::EVALUABLE_STATUSES.include?(synapse.status)
@@ -37,6 +45,16 @@ module Legion
37
45
  new_confidence = Helpers::Confidence.adjust(synapse.confidence, event)
38
46
  synapse.update(confidence: new_confidence)
39
47
 
48
+ # Step 5: Generate proposals if autonomous
49
+ if Helpers::Confidence.can_self_modify?(new_confidence) && Helpers::Proposals.reactive?
50
+ signal_record = Data::Model::SynapseSignal.where(synapse_id: synapse.id).order(Sequel.desc(:id)).first
51
+ propose_reactive(
52
+ synapse: synapse, payload: payload, signal_id: signal_record&.id,
53
+ attention_result: attention_result, transform_result: transform_result,
54
+ transformer_client: transformer_client
55
+ )
56
+ end
57
+
40
58
  {
41
59
  success: transform_result[:success],
42
60
  mode: mode,
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative '../data/models/synapse'
4
+ require_relative '../data/models/synapse_mutation'
4
5
  require_relative '../data/models/synapse_signal'
5
6
  require_relative '../helpers/confidence'
6
7
 
@@ -10,6 +11,7 @@ module Legion
10
11
  module Runners
11
12
  module GaiaReport
12
13
  def gaia_summary(**)
14
+ Data::Model.define_synapse_model
13
15
  synapses = Data::Model::Synapse.all
14
16
  active = synapses.select { |s| s.status == 'active' }
15
17
  dampened = synapses.select { |s| s.status == 'dampened' }
@@ -32,6 +34,7 @@ module Legion
32
34
  end
33
35
 
34
36
  def gaia_reflection(**)
37
+ Data::Model.define_synapse_mutation_model
35
38
  summary = gaia_summary
36
39
  recent_mutations = Data::Model::SynapseMutation
37
40
  .where { created_at >= Time.now - 3600 }
@@ -12,6 +12,8 @@ module Legion
12
12
  VALID_TRIGGERS = %w[hebbian pain dream gaia manual].freeze
13
13
 
14
14
  def mutate(synapse_id:, mutation_type:, changes:, trigger:)
15
+ Data::Model.define_synapse_model
16
+ Data::Model.define_synapse_mutation_model
15
17
  synapse = Data::Model::Synapse[synapse_id]
16
18
  return { success: false, error: 'synapse not found' } unless synapse
17
19
  return { success: false, error: "invalid mutation_type: #{mutation_type}" } unless VALID_MUTATION_TYPES.include?(mutation_type)
@@ -12,6 +12,8 @@ module Legion
12
12
  CONSECUTIVE_FAILURE_THRESHOLD = 3
13
13
 
14
14
  def handle_pain(synapse_id:, task_id: nil)
15
+ Data::Model.define_synapse_model
16
+ Data::Model.define_synapse_signal_model
15
17
  synapse = Data::Model::Synapse[synapse_id]
16
18
  return { success: false, error: 'synapse not found' } unless synapse
17
19
 
@@ -12,6 +12,8 @@ module Legion
12
12
  STABILITY_HOURS = 24
13
13
 
14
14
  def promote(synapse_id: nil, **)
15
+ Data::Model.define_synapse_model
16
+ Data::Model.define_synapse_mutation_model
15
17
  candidates = if synapse_id
16
18
  s = Data::Model::Synapse[synapse_id]
17
19
  s ? [s] : []
@@ -0,0 +1,253 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../helpers/confidence'
4
+ require_relative '../helpers/proposals'
5
+ require_relative '../data/models/synapse'
6
+ require_relative '../data/models/synapse_signal'
7
+ require_relative '../data/models/synapse_proposal'
8
+
9
+ module Legion
10
+ module Extensions
11
+ module Synapse
12
+ module Runners
13
+ module Propose
14
+ PAIN_CORRELATION_THRESHOLD = 3
15
+
16
+ def propose_reactive(synapse:, payload:, signal_id:, attention_result:, transform_result:,
17
+ transformer_client: nil)
18
+ Data::Model.define_synapse_proposal_model
19
+ return { proposals: [] } unless Helpers::Proposals.reactive?
20
+ return { proposals: [] } unless transformer_client
21
+
22
+ proposals = []
23
+
24
+ # Trigger 1: No transform template exists
25
+ proposals << propose_llm_transform(synapse, payload, signal_id, transformer_client) if synapse.transform.nil? || synapse.transform.to_s.strip.empty?
26
+
27
+ # Trigger 2: Transform failed
28
+ if transform_result[:success] == false && synapse.transform && !synapse.transform.to_s.strip.empty?
29
+ proposals << propose_transform_fix(synapse, payload, signal_id, transform_result, transformer_client)
30
+ end
31
+
32
+ # Trigger 3: Pain correlation — attention passed but recent downstream failures
33
+ proposals << propose_attention_adjustment(synapse, payload, signal_id, transformer_client) if attention_result[:passed] && pain_pattern?(synapse)
34
+
35
+ { proposals: proposals.compact }
36
+ end
37
+
38
+ def propose_proactive
39
+ Data::Model.define_synapse_model
40
+ Data::Model.define_synapse_proposal_model
41
+ Data::Model.define_synapse_signal_model
42
+ return { proposals: [] } unless Helpers::Proposals.proactive?
43
+
44
+ settings = Helpers::Proposals.settings
45
+ max_per_run = settings[:max_per_run] || 3
46
+ all_proposals = []
47
+
48
+ Data::Model::Synapse.where(status: 'active').all.each do |synapse|
49
+ next unless Helpers::Confidence.can_self_modify?(synapse.confidence)
50
+
51
+ count = 0
52
+ proposal = analyze_success_rate(synapse, settings)
53
+ if proposal
54
+ all_proposals << proposal
55
+ count += 1
56
+ end
57
+
58
+ if count < max_per_run
59
+ proposal = analyze_payload_drift(synapse, settings)
60
+ if proposal
61
+ all_proposals << proposal
62
+ count += 1
63
+ end
64
+ end
65
+
66
+ if count < max_per_run
67
+ proposal = analyze_routing(synapse)
68
+ all_proposals << proposal if proposal
69
+ end
70
+ end
71
+
72
+ { proposals: all_proposals }
73
+ end
74
+
75
+ private
76
+
77
+ def propose_llm_transform(synapse, payload, signal_id, transformer_client)
78
+ source_schema = infer_schema(payload)
79
+ target_schema = lookup_target_schema(synapse)
80
+ prompt = build_transform_prompt(source_schema, target_schema)
81
+
82
+ llm_result = call_llm(transformer_client, prompt)
83
+ create_proposal(
84
+ synapse: synapse, signal_id: signal_id, proposal_type: 'llm_transform',
85
+ trigger: 'reactive',
86
+ inputs: Legion::JSON.dump({ source_schema: source_schema, target_schema: target_schema }),
87
+ output: llm_result[:output],
88
+ rationale: 'no transform template exists for this synapse'
89
+ )
90
+ end
91
+
92
+ def propose_transform_fix(synapse, payload, signal_id, transform_result, transformer_client)
93
+ source_schema = infer_schema(payload)
94
+ current_transform = synapse.transform
95
+ errors = transform_result[:error]
96
+ prompt = build_fix_prompt(current_transform, source_schema, errors)
97
+
98
+ llm_result = call_llm(transformer_client, prompt)
99
+ create_proposal(
100
+ synapse: synapse, signal_id: signal_id, proposal_type: 'transform_mutation',
101
+ trigger: 'reactive',
102
+ inputs: Legion::JSON.dump({ current_transform: current_transform, errors: errors, payload_schema: source_schema }),
103
+ output: llm_result[:output],
104
+ rationale: "transform failed: #{Array(errors).first}"
105
+ )
106
+ end
107
+
108
+ def propose_attention_adjustment(synapse, payload, signal_id, transformer_client)
109
+ recent_failures = recent_failed_signals(synapse, 10)
110
+ prompt = build_attention_prompt(synapse.attention, payload, recent_failures)
111
+
112
+ llm_result = call_llm(transformer_client, prompt)
113
+ create_proposal(
114
+ synapse: synapse, signal_id: signal_id, proposal_type: 'attention_mutation',
115
+ trigger: 'reactive',
116
+ inputs: Legion::JSON.dump({ current_attention: synapse.attention, recent_failures: recent_failures.size }),
117
+ output: llm_result[:output],
118
+ rationale: "#{recent_failures.size} recent downstream failures despite attention passing"
119
+ )
120
+ end
121
+
122
+ def analyze_success_rate(synapse, settings)
123
+ threshold = settings[:success_rate_threshold] || 0.8
124
+ signals = Data::Model::SynapseSignal.where(synapse_id: synapse.id).order(Sequel.desc(:id)).limit(100).all
125
+ return nil if signals.size < 10
126
+
127
+ success_count = signals.count(&:transform_success)
128
+ rate = success_count.to_f / signals.size
129
+ return nil if rate >= threshold
130
+ return nil if dedup_exists?(synapse.id, 'transform_mutation', 'proactive', settings)
131
+
132
+ create_proposal(
133
+ synapse: synapse, signal_id: nil, proposal_type: 'transform_mutation',
134
+ trigger: 'proactive',
135
+ inputs: Legion::JSON.dump({ success_rate: rate.round(3), sample_size: signals.size, threshold: threshold }),
136
+ output: nil,
137
+ rationale: "success rate #{(rate * 100).round(1)}% below threshold #{(threshold * 100).round(1)}%"
138
+ )
139
+ end
140
+
141
+ def analyze_payload_drift(synapse, settings)
142
+ return nil if synapse.transform.nil? || synapse.transform.to_s.strip.empty?
143
+
144
+ drift_threshold = settings[:payload_drift_threshold] || 0.2
145
+ signals = Data::Model::SynapseSignal.where(synapse_id: synapse.id).order(Sequel.desc(:id)).limit(50).all
146
+ return nil if signals.size < 10
147
+
148
+ failed = signals.count { |s| !s.transform_success }
149
+ drift_rate = failed.to_f / signals.size
150
+ return nil if drift_rate < drift_threshold
151
+ return nil if dedup_exists?(synapse.id, 'transform_mutation', 'proactive', settings)
152
+
153
+ create_proposal(
154
+ synapse: synapse, signal_id: nil, proposal_type: 'transform_mutation',
155
+ trigger: 'proactive',
156
+ inputs: Legion::JSON.dump({ drift_rate: drift_rate.round(3), sample_size: signals.size }),
157
+ output: nil,
158
+ rationale: "payload drift detected: #{(drift_rate * 100).round(1)}% transform failures in recent signals"
159
+ )
160
+ end
161
+
162
+ def analyze_routing(synapse)
163
+ return nil if dedup_exists?(synapse.id, 'route_change', 'proactive', Helpers::Proposals.settings)
164
+
165
+ signals = Data::Model::SynapseSignal.where(synapse_id: synapse.id).order(Sequel.desc(:id)).limit(50).all
166
+ return nil if signals.size < 10
167
+
168
+ nil
169
+ end
170
+
171
+ def pain_pattern?(synapse)
172
+ recent = recent_failed_signals(synapse, 20)
173
+ recent.size >= PAIN_CORRELATION_THRESHOLD
174
+ end
175
+
176
+ def recent_failed_signals(synapse, limit)
177
+ Data::Model::SynapseSignal.where(
178
+ synapse_id: synapse.id, downstream_outcome: 'failed'
179
+ ).order(Sequel.desc(:id)).limit(limit).all
180
+ end
181
+
182
+ def dedup_exists?(synapse_id, proposal_type, trigger, settings)
183
+ window = settings[:dedup_window_hours] || 24
184
+ cutoff = Time.now - (window * 3600)
185
+ Data::Model::SynapseProposal.where(
186
+ synapse_id: synapse_id, proposal_type: proposal_type,
187
+ trigger: trigger, status: 'pending'
188
+ ).where(Sequel.lit('created_at >= ?', cutoff)).any?
189
+ end
190
+
191
+ def create_proposal(synapse:, signal_id:, proposal_type:, trigger:, inputs:, output:, rationale:)
192
+ Data::Model::SynapseProposal.create(
193
+ synapse_id: synapse.id,
194
+ signal_id: signal_id,
195
+ proposal_type: proposal_type,
196
+ trigger: trigger,
197
+ inputs: inputs,
198
+ output: output,
199
+ rationale: rationale,
200
+ status: 'pending'
201
+ )
202
+ end
203
+
204
+ def call_llm(transformer_client, prompt)
205
+ return { output: nil } unless transformer_client
206
+
207
+ engine_options = Helpers::Proposals.llm_engine_options
208
+ result = transformer_client.transform(
209
+ transformation: prompt, payload: {}, engine: :llm, engine_options: engine_options
210
+ )
211
+ { output: result[:success] ? Legion::JSON.dump(result[:result]) : nil }
212
+ rescue StandardError => e
213
+ Legion::Logging.warn("Proposal LLM call failed: #{e.message}")
214
+ { output: nil }
215
+ end
216
+
217
+ def infer_schema(payload)
218
+ return {} unless payload.is_a?(Hash)
219
+
220
+ payload.transform_values { |v| v.class.name }
221
+ end
222
+
223
+ def lookup_target_schema(synapse)
224
+ return {} unless synapse.target_function_id
225
+ return {} unless defined?(Legion::Extensions::Lex)
226
+
227
+ {}
228
+ end
229
+
230
+ def build_transform_prompt(source_schema, target_schema)
231
+ "Given a payload with schema: #{Legion::JSON.dump(source_schema)}, " \
232
+ "generate a JSON transform template that maps it to target schema: #{Legion::JSON.dump(target_schema)}. " \
233
+ 'Return only the JSON template string, no explanation.'
234
+ end
235
+
236
+ def build_fix_prompt(current_transform, source_schema, errors)
237
+ "The current transform template is: #{current_transform}. " \
238
+ "It failed with errors: #{Array(errors).join(', ')}. " \
239
+ "The payload schema is: #{Legion::JSON.dump(source_schema)}. " \
240
+ 'Suggest a corrected transform template. Return only the JSON template string.'
241
+ end
242
+
243
+ def build_attention_prompt(current_attention, payload, recent_failures)
244
+ "The current attention rules are: #{current_attention}. " \
245
+ "Recent payload example: #{Legion::JSON.dump(payload)}. " \
246
+ "There have been #{recent_failures.size} downstream failures despite attention passing. " \
247
+ 'Suggest refined attention rules as a JSON condition object. Return only JSON.'
248
+ end
249
+ end
250
+ end
251
+ end
252
+ end
253
+ end
@@ -10,6 +10,9 @@ module Legion
10
10
  module Runners
11
11
  module Report
12
12
  def report(synapse_id:)
13
+ Data::Model.define_synapse_model
14
+ Data::Model.define_synapse_mutation_model
15
+ Data::Model.define_synapse_signal_model
13
16
  synapse = Data::Model::Synapse[synapse_id]
14
17
  return { success: false, error: 'synapse not found' } unless synapse
15
18
 
@@ -11,6 +11,7 @@ module Legion
11
11
  SEED_CONFIDENCE_THRESHOLD = 0.7
12
12
 
13
13
  def retrieve_and_seed(knowledge_entries:, **)
14
+ Data::Model.define_synapse_model
14
15
  seeded = []
15
16
 
16
17
  knowledge_entries.each do |entry|
@@ -9,6 +9,8 @@ module Legion
9
9
  module Runners
10
10
  module Revert
11
11
  def revert(synapse_id:, to_version: nil, trigger: 'pain')
12
+ Data::Model.define_synapse_model
13
+ Data::Model.define_synapse_mutation_model
12
14
  synapse = Data::Model::Synapse[synapse_id]
13
15
  return { success: false, error: 'synapse not found' } unless synapse
14
16
 
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Synapse
6
- VERSION = '0.2.2'
6
+ VERSION = '0.3.0'
7
7
  end
8
8
  end
9
9
  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.2.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Esity
@@ -29,14 +29,14 @@ dependencies:
29
29
  requirements:
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
- version: 0.2.0
32
+ version: 0.3.0
33
33
  type: :runtime
34
34
  prerelease: false
35
35
  version_requirements: !ruby/object:Gem::Requirement
36
36
  requirements:
37
37
  - - ">="
38
38
  - !ruby/object:Gem::Version
39
- version: 0.2.0
39
+ version: 0.3.0
40
40
  description: Attention, transformation, and routing with confidence scoring, pain
41
41
  signals, homeostasis, and self-governance
42
42
  email:
@@ -61,15 +61,19 @@ files:
61
61
  - lib/legion/extensions/synapse/actors/evaluate.rb
62
62
  - lib/legion/extensions/synapse/actors/homeostasis.rb
63
63
  - lib/legion/extensions/synapse/actors/pain.rb
64
+ - lib/legion/extensions/synapse/actors/propose.rb
64
65
  - lib/legion/extensions/synapse/client.rb
65
66
  - lib/legion/extensions/synapse/data/migrations/001_create_synapses.rb
66
67
  - lib/legion/extensions/synapse/data/migrations/002_create_synapse_mutations.rb
67
68
  - lib/legion/extensions/synapse/data/migrations/003_create_synapse_signals.rb
69
+ - lib/legion/extensions/synapse/data/migrations/004_create_synapse_proposals.rb
68
70
  - lib/legion/extensions/synapse/data/models/synapse.rb
69
71
  - lib/legion/extensions/synapse/data/models/synapse_mutation.rb
72
+ - lib/legion/extensions/synapse/data/models/synapse_proposal.rb
70
73
  - lib/legion/extensions/synapse/data/models/synapse_signal.rb
71
74
  - lib/legion/extensions/synapse/helpers/confidence.rb
72
75
  - lib/legion/extensions/synapse/helpers/homeostasis.rb
76
+ - lib/legion/extensions/synapse/helpers/proposals.rb
73
77
  - lib/legion/extensions/synapse/helpers/relationship_wrapper.rb
74
78
  - lib/legion/extensions/synapse/runners/crystallize.rb
75
79
  - lib/legion/extensions/synapse/runners/dream.rb
@@ -78,6 +82,7 @@ files:
78
82
  - lib/legion/extensions/synapse/runners/mutate.rb
79
83
  - lib/legion/extensions/synapse/runners/pain.rb
80
84
  - lib/legion/extensions/synapse/runners/promote.rb
85
+ - lib/legion/extensions/synapse/runners/propose.rb
81
86
  - lib/legion/extensions/synapse/runners/report.rb
82
87
  - lib/legion/extensions/synapse/runners/retrieve.rb
83
88
  - lib/legion/extensions/synapse/runners/revert.rb