lex-synapse 0.2.3 → 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: 15e9a0134efeddb2987af089e90c91d5f954524aa44c147ec5e11ae9eda9fd0f
4
- data.tar.gz: b08d225fdd4d3db84155deb3a707dcc3f6f9ebfeb6e34cb1e71e057c1ef54801
3
+ metadata.gz: 60a95b064e889f6594f48ea66c0c12e8360b4679ef9daf572e31560e304ea277
4
+ data.tar.gz: a96ea18996d7b3d87ee309cbba68f2258b34f40fa9a0a61068562f81f347177d
5
5
  SHA512:
6
- metadata.gz: c6a75eff3fff57732dd4c50f2e5d2f9b94fde785ae4b5f7a3d7eedb3937eb96983c5ef1bb4f85f55229b744e80a4c98f779e69fca4d6cec3ff1fb3568694533a
7
- data.tar.gz: da3a2b72741ddc1ac70319e3c2693c77ab050bc5c4b6fd6cefee301ba12bdfe820667356634de93ef8c614ea4a56e1e54f5761cc47ba76d42c1e32ae0ccb0745
6
+ metadata.gz: 9faa202ea887493c0ab6b3b9df9323757ca00e1fca347b0669876241ef4733aa4f333c5153c8b0a46d59fd28d6ba5e5286f5e7d541dc47d21e65ea4a9b6f554a
7
+ data.tar.gz: 85411a3ec047e7d9e944b61d08c03ab3ee3868f58887b6e13c3e6ab46d1fcad3ef5a56ddf73d12cc8a32a77b4d1c2d34386caa9d2711950c464a818229fdb495
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
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
+
3
23
  ## [0.2.3] - 2026-03-19
4
24
 
5
25
  ### 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,7 +21,8 @@ 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
@@ -32,7 +33,8 @@ Legion::Extensions::Synapse
32
33
  │ ├── Dream # replay historical signals in simulation mode; replay/simulate
33
34
  │ ├── GaiaReport # GAIA tick hook: report confidence and health per synapse
34
35
  │ ├── Promote # Apollo integration: promote high-confidence synapse patterns to shared knowledge
35
- └── Retrieve # Apollo integration: retrieve relevant synapse patterns from shared knowledge
36
+ ├── Retrieve # Apollo integration: retrieve relevant synapse patterns from shared knowledge
37
+ │ └── Propose # reactive (signal-driven) + proactive (periodic) proposal generation
36
38
  ├── Helpers/
37
39
  │ ├── Confidence # scoring, adjustments, autonomy ranges, decay
38
40
  │ ├── Homeostasis # spike/drought detection, baseline tracking
@@ -79,6 +81,16 @@ Legion::Extensions::Synapse
79
81
  - **synapse_mutations**: Versioned change history with before/after JSON snapshots
80
82
  - **synapse_signals**: Per-signal outcome records (attention pass, transform success, latency, downstream outcome)
81
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
+
82
94
  ## GAIA / Apollo Integration (v0.2.2)
83
95
 
84
96
  - **GaiaReport runner**: Called during the GAIA tick cycle to report per-synapse confidence and health metrics.
@@ -91,18 +103,18 @@ Legion::Extensions::Synapse
91
103
  | Gem | Purpose |
92
104
  |-----|---------|
93
105
  | `lex-conditioner` >= 0.3.0 | Attention evaluation (condition rules) |
94
- | `lex-transformer` >= 0.2.0 | Payload transformation (template engines) |
106
+ | `lex-transformer` >= 0.3.0 | Payload transformation (template engines) |
95
107
  | `legion-data` | Required — database persistence via Sequel |
96
108
 
97
109
  ## Testing
98
110
 
99
111
  ```bash
100
112
  bundle install
101
- bundle exec rspec # 308 specs, 0 failures
113
+ bundle exec rspec # 366 specs, 0 failures
102
114
  bundle exec rubocop # 0 offenses
103
115
  ```
104
116
 
105
- 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.
106
118
 
107
119
  ---
108
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
 
@@ -57,6 +61,24 @@ module Legion
57
61
  status: origin == 'emergent' ? 'observing' : 'active'
58
62
  )
59
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
60
82
  end
61
83
  end
62
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
@@ -17,6 +17,8 @@ module Legion
17
17
  key: :synapse_id
18
18
  one_to_many :signals, class: 'Legion::Extensions::Synapse::Data::Model::SynapseSignal',
19
19
  key: :synapse_id
20
+ one_to_many :proposals, class: 'Legion::Extensions::Synapse::Data::Model::SynapseProposal',
21
+ key: :synapse_id
20
22
  end
21
23
  klass.set_primary_key :id
22
24
  const_set(:Synapse, klass)
@@ -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
@@ -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
@@ -1,18 +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'
5
6
  require_relative '../data/models/synapse_mutation'
6
7
  require_relative '../data/models/synapse_signal'
8
+ require_relative 'propose'
7
9
 
8
10
  module Legion
9
11
  module Extensions
10
12
  module Synapse
11
13
  module Runners
12
14
  module Evaluate
15
+ include Propose
16
+
13
17
  def evaluate(synapse_id:, payload: {}, conditioner_client: nil, transformer_client: nil)
14
18
  Data::Model.define_synapse_model
15
19
  Data::Model.define_synapse_signal_model
20
+ Data::Model.define_synapse_proposal_model
16
21
  synapse = Data::Model::Synapse[synapse_id]
17
22
  return { success: false, error: 'synapse not found' } unless synapse
18
23
  return { success: false, error: 'synapse not active' } unless Helpers::Confidence::EVALUABLE_STATUSES.include?(synapse.status)
@@ -40,6 +45,16 @@ module Legion
40
45
  new_confidence = Helpers::Confidence.adjust(synapse.confidence, event)
41
46
  synapse.update(confidence: new_confidence)
42
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
+
43
58
  {
44
59
  success: transform_result[:success],
45
60
  mode: mode,
@@ -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
@@ -3,7 +3,7 @@
3
3
  module Legion
4
4
  module Extensions
5
5
  module Synapse
6
- VERSION = '0.2.3'
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.3
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