legion-gaia 0.9.63 → 0.9.66
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 +32 -0
- data/lib/legion/gaia/advisory.rb +60 -2
- data/lib/legion/gaia/disclosure.rb +133 -0
- data/lib/legion/gaia/routes.rb +12 -1
- data/lib/legion/gaia/version.rb +1 -1
- data/lib/legion/gaia/visible_growth.rb +202 -0
- data/lib/legion/gaia.rb +141 -14
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: dc0229532bc53d2868fac7cc6279bb2875ceaaedbb63269dc8b5a742a7c95832
|
|
4
|
+
data.tar.gz: da7e11d18691d6af5e90e2c4da209feaf1b6ab79b4aaa91f4c409e517d7428cf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c87775e2f41b815da9b996ceca3383dbf956ec8c8e643c26ec8390fb7477fc0ead9739b4802cbdc5984ebf1266f1d5dd22d145c7b657cc8e7712035f0496932c
|
|
7
|
+
data.tar.gz: e03a41455d7d36dbb49d89e2b7c74e3db930c07a1a730cb4c695a088c446be020463ff0ea588bdea1ce01b06504890890afa08c42344fbd16e71b6818058727b
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,37 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.9.66] - 2026-07-24
|
|
4
|
+
### Added
|
|
5
|
+
- `Gaia.observe_from_pipeline(identity:, caller:, exchange_id:)` — pipeline observation for API clients; reinforces bond, triggers coldstart, registers duckling partner on first human request
|
|
6
|
+
- Bounded idempotency (FIFO hash, 10k max) prevents duplicate observations per exchange_id
|
|
7
|
+
- Only fires for `:human` type callers; system/internal calls are ignored
|
|
8
|
+
|
|
9
|
+
## [0.9.65] - 2026-07-23
|
|
10
|
+
### Fixed
|
|
11
|
+
- `resolve_process_identity` now uses `Legion::Identity::Process.canonical_name` exclusively — never `Etc.loginname` or `ENV['USER']` (spoofable, wrong string in corporate environments)
|
|
12
|
+
- Provisional partner registration skips gracefully when identity hasn't resolved yet (no registration under wrong/anonymous string)
|
|
13
|
+
- `drain_growth_frames` moved to public API (Advisory calls it directly)
|
|
14
|
+
- Removed `private_class_method` from Advisory — all methods callable
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
- Advisory now produces `:system_prompt` with partner model slots, growth frames, and epistemic qualifiers — injected via existing legion-llm `gaia_advisory` step (zero legion-llm changes)
|
|
18
|
+
- `Advisory.build_partner_system_prompt(identity:)` — assembles working-memory context for the LLM
|
|
19
|
+
- `Advisory.build_partner_slots(identity:)` — renders PartnerModel slots as natural language
|
|
20
|
+
- `Advisory.drain_growth_content` — drains and renders pending milestone/growth frames
|
|
21
|
+
- `Advisory.build_epistemic_qualifier(identity:)` — delegates to VisibleGrowth for uncertainty markers
|
|
22
|
+
- Log line: `[gaia] advisory partner_prompt injected identity=X length=N` when partner context fires
|
|
23
|
+
|
|
24
|
+
## [0.9.64] - 2026-07-22
|
|
25
|
+
### Added
|
|
26
|
+
- H9: `Disclosure.report(identity:)` — full partner model transparency surface; includes bond_state, all behavioral_synapses with autonomy_mode, preferences, calibration_weights, imprint_state, prediction_accuracy; each section soft-guarded (nil if extension not loaded, no crash); always includes data_locality statement and termination_available: true
|
|
27
|
+
- H9: `VisibleGrowth.milestone_acknowledgment(identity:, domain:, new_mode:, old_mode:)` — fires once per tier transition per (identity, domain); returns natural-language acknowledgment ("I've noticed you prefer shorter answers — I'm now adjusting my approach.")
|
|
28
|
+
- H9: `VisibleGrowth.pain_revert_acknowledgment(domain:)` — called after 3 consecutive failures reset a synapse; "I've been getting [domain] wrong — I've reset that."
|
|
29
|
+
- H9: `VisibleGrowth.graduation_acknowledgment(identity:)` — fires once per identity when imprint window closes
|
|
30
|
+
- H9: `VisibleGrowth.onboarding_frame(identity:)` — first-contact transparency; plain language about learning, local storage, and termination option; fires once per identity
|
|
31
|
+
- H9: `VisibleGrowth.epistemic_qualifier(identity:, domain:)` — returns hedge/qualifier string during imprint or when relevant synapse is observe-tier; nil when confident (default post-imprint)
|
|
32
|
+
- Wire milestone and pain surfaces into `grade_behavioral_synapses`; pending frames stored in `@pending_growth_frames` (drain via `Gaia.drain_growth_frames`)
|
|
33
|
+
- `GET /api/gaia/partner/:identity/report` route — returns full Disclosure.report for any identity
|
|
34
|
+
|
|
3
35
|
## [0.9.63] - 2026-07-22
|
|
4
36
|
### Added
|
|
5
37
|
- H8: `DeathProtocol.terminate_bond(identity:, confirm:)` — single entry point for bond termination; walks cascade across all stores (bond, behavioral_synapses, session, audit, attribution, calibration, preferences, memory_traces, predictions, trust); returns proof-of-destruction receipt with per-store results; emits `gaia.bond.terminated` event
|
data/lib/legion/gaia/advisory.rb
CHANGED
|
@@ -20,6 +20,14 @@ module Legion
|
|
|
20
20
|
advisory[:valence] = Gaia.last_valences if Gaia.last_valences
|
|
21
21
|
merge_tick_data!(advisory, Gaia.registry&.tick_host&.last_tick_result)
|
|
22
22
|
merge_observer_data!(advisory, caller)
|
|
23
|
+
|
|
24
|
+
identity = caller&.dig(:requested_by, :identity)
|
|
25
|
+
partner_prompt = build_partner_system_prompt(identity: identity) if identity
|
|
26
|
+
if partner_prompt
|
|
27
|
+
advisory[:system_prompt] = partner_prompt
|
|
28
|
+
log.info("[gaia] advisory partner_prompt injected identity=#{identity} length=#{partner_prompt.length}")
|
|
29
|
+
end
|
|
30
|
+
|
|
23
31
|
result = advisory.compact
|
|
24
32
|
if result.any?
|
|
25
33
|
log.info("GAIA advisory generated conversation_id=#{conversation_id} keys=#{result.keys.join(',')}")
|
|
@@ -94,8 +102,58 @@ module Legion
|
|
|
94
102
|
nil
|
|
95
103
|
end
|
|
96
104
|
|
|
97
|
-
|
|
98
|
-
|
|
105
|
+
def build_partner_system_prompt(identity:)
|
|
106
|
+
log.unknown("[gaia] build_partner_system_prompt identity=#{identity} " \
|
|
107
|
+
"bond_registry_defined=#{defined?(Legion::Gaia::BondRegistry)} " \
|
|
108
|
+
"partner=#{defined?(Legion::Gaia::BondRegistry) && BondRegistry.partner?(identity.to_s)}")
|
|
109
|
+
|
|
110
|
+
return nil unless defined?(Legion::Gaia::BondRegistry) && BondRegistry.partner?(identity.to_s)
|
|
111
|
+
|
|
112
|
+
parts = ['|gaia:active|']
|
|
113
|
+
|
|
114
|
+
slots = build_partner_slots(identity: identity)
|
|
115
|
+
parts << slots if slots
|
|
116
|
+
log.unknown("[gaia] partner_slots count=#{slots ? 'present' : 'empty'}")
|
|
117
|
+
|
|
118
|
+
growth = drain_growth_content
|
|
119
|
+
parts << growth if growth
|
|
120
|
+
|
|
121
|
+
qualifier = build_epistemic_qualifier(identity: identity)
|
|
122
|
+
parts << qualifier if qualifier
|
|
123
|
+
|
|
124
|
+
parts.join("\n\n")
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def build_partner_slots(identity:)
|
|
128
|
+
return nil unless defined?(Legion::Gaia::PartnerModel)
|
|
129
|
+
|
|
130
|
+
slots = PartnerModel.build(identity: identity)
|
|
131
|
+
return nil if slots.empty?
|
|
132
|
+
|
|
133
|
+
lines = slots.map do |slot|
|
|
134
|
+
domain = slot[:domain]
|
|
135
|
+
content = slot[:content]
|
|
136
|
+
content_str = content.is_a?(Hash) ? content.map { |k, v| "#{k}: #{v}" }.join(', ') : content.to_s
|
|
137
|
+
"- #{domain}: #{content_str}"
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
"What I know about working with you:\n#{lines.join("\n")}"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def drain_growth_content
|
|
144
|
+
return nil unless Gaia.respond_to?(:drain_growth_frames)
|
|
145
|
+
|
|
146
|
+
frames = Gaia.drain_growth_frames
|
|
147
|
+
return nil if frames.nil? || frames.empty?
|
|
148
|
+
|
|
149
|
+
frames.join("\n")
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def build_epistemic_qualifier(identity:)
|
|
153
|
+
return nil unless defined?(Legion::Gaia::VisibleGrowth)
|
|
154
|
+
|
|
155
|
+
VisibleGrowth.epistemic_qualifier(identity: identity)
|
|
156
|
+
end
|
|
99
157
|
end
|
|
100
158
|
end
|
|
101
159
|
end
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Gaia
|
|
5
|
+
module Disclosure
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# Full partner model report — what the agent knows, how it was earned, what stays local.
|
|
9
|
+
# Principal-bound: caller reads ONLY their own partner model.
|
|
10
|
+
#
|
|
11
|
+
# @param identity [String] the partner identity key
|
|
12
|
+
# @return [Hash]
|
|
13
|
+
def report(identity:)
|
|
14
|
+
identity_str = identity.to_s
|
|
15
|
+
|
|
16
|
+
{
|
|
17
|
+
identity: identity_str,
|
|
18
|
+
bond_state: fetch_bond_state(identity_str),
|
|
19
|
+
behavioral_synapses: fetch_behavioral_synapses(identity_str),
|
|
20
|
+
preferences: fetch_preferences(identity_str),
|
|
21
|
+
calibration_weights: fetch_calibration_weights(identity_str),
|
|
22
|
+
imprint_state: fetch_imprint_state,
|
|
23
|
+
prediction_accuracy: fetch_prediction_accuracy(identity_str),
|
|
24
|
+
data_locality: 'All data is stored locally on this node. Nothing leaves this machine.',
|
|
25
|
+
termination_available: true
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# --- private section ---
|
|
30
|
+
|
|
31
|
+
def fetch_bond_state(identity)
|
|
32
|
+
entry = Legion::Gaia::BondRegistry.instance_variable_get(:@bonds)[identity]
|
|
33
|
+
return nil unless entry
|
|
34
|
+
|
|
35
|
+
{
|
|
36
|
+
bond: entry[:bond],
|
|
37
|
+
strength: entry[:strength],
|
|
38
|
+
origin: entry[:origin],
|
|
39
|
+
reinforcement_count: entry[:reinforcement_count],
|
|
40
|
+
since: entry[:since],
|
|
41
|
+
preferred_channel: entry[:preferred_channel],
|
|
42
|
+
lifecycle_state: Legion::Gaia::BondRegistry.bond_state(identity)
|
|
43
|
+
}
|
|
44
|
+
rescue StandardError => e
|
|
45
|
+
Legion::Gaia::Disclosure.handle_exception_quietly(e, 'disclosure.fetch_bond_state')
|
|
46
|
+
nil
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def fetch_behavioral_synapses(identity)
|
|
50
|
+
all = Legion::Gaia::BehavioralSynapse.all_for(identity: identity)
|
|
51
|
+
return nil if all.empty?
|
|
52
|
+
|
|
53
|
+
all.map do |entry|
|
|
54
|
+
{
|
|
55
|
+
id: entry[:id],
|
|
56
|
+
domain: entry[:domain],
|
|
57
|
+
directive: entry[:directive],
|
|
58
|
+
confidence: entry[:confidence],
|
|
59
|
+
autonomy_mode: Legion::Gaia::BehavioralSynapse::Math.autonomy_mode(entry[:confidence].to_f),
|
|
60
|
+
status: entry[:status],
|
|
61
|
+
origin: entry[:origin],
|
|
62
|
+
consecutive_successes: entry[:consecutive_successes],
|
|
63
|
+
consecutive_failures: entry[:consecutive_failures],
|
|
64
|
+
last_reinforced_at: entry[:last_reinforced_at]
|
|
65
|
+
}
|
|
66
|
+
end
|
|
67
|
+
rescue StandardError => e
|
|
68
|
+
Legion::Gaia::Disclosure.handle_exception_quietly(e, 'disclosure.fetch_behavioral_synapses')
|
|
69
|
+
nil
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def fetch_preferences(identity)
|
|
73
|
+
return nil unless defined?(Legion::Extensions::Mesh::Helpers::PreferenceProfile)
|
|
74
|
+
|
|
75
|
+
result = Legion::Extensions::Mesh::Helpers::PreferenceProfile.for_owner(owner_id: identity)
|
|
76
|
+
return nil unless result.is_a?(Hash) && result[:directives].is_a?(Array)
|
|
77
|
+
|
|
78
|
+
result[:directives]
|
|
79
|
+
rescue StandardError => e
|
|
80
|
+
Legion::Gaia::Disclosure.handle_exception_quietly(e, 'disclosure.fetch_preferences')
|
|
81
|
+
nil
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def fetch_calibration_weights(identity)
|
|
85
|
+
return nil unless defined?(Legion::Extensions::Agentic::Social::Calibration::Runners::Calibration)
|
|
86
|
+
|
|
87
|
+
runner = Object.new
|
|
88
|
+
runner.extend(Legion::Extensions::Agentic::Social::Calibration::Runners::Calibration)
|
|
89
|
+
result = runner.respond_to?(:calibration_weights_for) ? runner.calibration_weights_for(identity: identity) : nil
|
|
90
|
+
result.is_a?(Hash) ? result : nil
|
|
91
|
+
rescue StandardError => e
|
|
92
|
+
Legion::Gaia::Disclosure.handle_exception_quietly(e, 'disclosure.fetch_calibration_weights')
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def fetch_imprint_state
|
|
97
|
+
return nil unless defined?(Legion::Extensions::Coldstart)
|
|
98
|
+
return nil unless Legion::Extensions::Coldstart.respond_to?(:connected?) &&
|
|
99
|
+
Legion::Extensions::Coldstart.connected?
|
|
100
|
+
|
|
101
|
+
bootstrap = Legion::Extensions::Coldstart::Helpers::Bootstrap.instance
|
|
102
|
+
{
|
|
103
|
+
layer: bootstrap.respond_to?(:current_layer) ? bootstrap.current_layer : nil,
|
|
104
|
+
observations_count: bootstrap.respond_to?(:observation_count) ? bootstrap.observation_count : nil,
|
|
105
|
+
active: bootstrap.respond_to?(:imprint_active?) ? bootstrap.imprint_active? : nil
|
|
106
|
+
}
|
|
107
|
+
rescue StandardError => e
|
|
108
|
+
Legion::Gaia::Disclosure.handle_exception_quietly(e, 'disclosure.fetch_imprint_state')
|
|
109
|
+
nil
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def fetch_prediction_accuracy(identity)
|
|
113
|
+
return nil unless defined?(Legion::Extensions::AgenticInference)
|
|
114
|
+
return nil unless Legion::Extensions::AgenticInference.respond_to?(:accuracy_for)
|
|
115
|
+
|
|
116
|
+
Legion::Extensions::AgenticInference.accuracy_for(identity: identity)
|
|
117
|
+
rescue StandardError => e
|
|
118
|
+
Legion::Gaia::Disclosure.handle_exception_quietly(e, 'disclosure.fetch_prediction_accuracy')
|
|
119
|
+
nil
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def handle_exception_quietly(exception, operation)
|
|
123
|
+
return unless defined?(Legion::Logging)
|
|
124
|
+
|
|
125
|
+
Legion::Logging.debug("[gaia] #{operation} soft-guarded: #{exception.class}: #{exception.message}")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
module_function :fetch_bond_state, :fetch_behavioral_synapses, :fetch_preferences,
|
|
129
|
+
:fetch_calibration_weights, :fetch_imprint_state, :fetch_prediction_accuracy,
|
|
130
|
+
:handle_exception_quietly
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
data/lib/legion/gaia/routes.rb
CHANGED
|
@@ -63,6 +63,7 @@ module Legion
|
|
|
63
63
|
register_teams_webhook_route(app)
|
|
64
64
|
register_ingest_route(app)
|
|
65
65
|
register_bond_terminate_route(app)
|
|
66
|
+
register_partner_report_route(app)
|
|
66
67
|
end
|
|
67
68
|
|
|
68
69
|
def self.register_status_route(app)
|
|
@@ -204,10 +205,20 @@ module Legion
|
|
|
204
205
|
end
|
|
205
206
|
end
|
|
206
207
|
|
|
208
|
+
def self.register_partner_report_route(app)
|
|
209
|
+
app.get '/api/gaia/partner/:identity/report' do
|
|
210
|
+
identity = params[:identity]
|
|
211
|
+
halt 400, json_error('missing_identity', 'identity is required') if identity.to_s.empty?
|
|
212
|
+
|
|
213
|
+
result = Legion::Gaia::Disclosure.report(identity: identity)
|
|
214
|
+
json_response(result)
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
207
218
|
class << self
|
|
208
219
|
private :register_status_route, :register_ticks_route, :register_channels_route,
|
|
209
220
|
:register_buffer_route, :register_sessions_route, :register_teams_webhook_route,
|
|
210
|
-
:register_ingest_route, :register_bond_terminate_route
|
|
221
|
+
:register_ingest_route, :register_bond_terminate_route, :register_partner_report_route
|
|
211
222
|
end
|
|
212
223
|
end
|
|
213
224
|
end
|
data/lib/legion/gaia/version.rb
CHANGED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Legion
|
|
4
|
+
module Gaia
|
|
5
|
+
module VisibleGrowth
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
# In-memory deduplication sets — reset on restart is intentional (worst case: fires twice)
|
|
9
|
+
@acknowledged_milestones = Set.new
|
|
10
|
+
@graduated_identities = Set.new
|
|
11
|
+
@onboarded_identities = Set.new
|
|
12
|
+
@mutex = Mutex.new
|
|
13
|
+
|
|
14
|
+
TIER_LABELS = {
|
|
15
|
+
observe: 'noticing patterns',
|
|
16
|
+
filter: 'adjusting my approach',
|
|
17
|
+
transform: 'actively shaping my responses',
|
|
18
|
+
autonomous: 'acting on this without asking'
|
|
19
|
+
}.freeze
|
|
20
|
+
|
|
21
|
+
DOMAIN_PHRASES = {
|
|
22
|
+
'brevity' => 'shorter answers',
|
|
23
|
+
'verbosity' => 'longer, more detailed answers',
|
|
24
|
+
'tone' => 'the tone',
|
|
25
|
+
'format' => 'the format',
|
|
26
|
+
'code' => 'code style',
|
|
27
|
+
'detail' => 'the level of detail'
|
|
28
|
+
}.freeze
|
|
29
|
+
|
|
30
|
+
# Called when a synapse confidence crosses into a new autonomy tier.
|
|
31
|
+
# Fires ONCE per (identity, domain, tier transition).
|
|
32
|
+
#
|
|
33
|
+
# @param identity [String]
|
|
34
|
+
# @param domain [String]
|
|
35
|
+
# @param new_mode [Symbol] :observe | :filter | :transform | :autonomous
|
|
36
|
+
# @param old_mode [Symbol]
|
|
37
|
+
# @return [String, nil] acknowledgment message, or nil if already fired
|
|
38
|
+
def milestone_acknowledgment(identity:, domain:, new_mode:, old_mode:)
|
|
39
|
+
key = "#{identity}:#{domain}:#{old_mode}->#{new_mode}"
|
|
40
|
+
@mutex.synchronize do
|
|
41
|
+
return nil if @acknowledged_milestones.include?(key)
|
|
42
|
+
|
|
43
|
+
@acknowledged_milestones.add(key)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
phrase = domain_phrase(domain)
|
|
47
|
+
tier_desc = TIER_LABELS.fetch(new_mode.to_sym, new_mode.to_s)
|
|
48
|
+
|
|
49
|
+
"I've noticed you prefer #{phrase} — I'm now #{tier_desc}. Tell me if I've got that wrong."
|
|
50
|
+
rescue StandardError => e
|
|
51
|
+
VisibleGrowth.log_quietly(e, 'visible_growth.milestone_acknowledgment')
|
|
52
|
+
nil
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Called when pain fires (3 consecutive failures → dampened status).
|
|
56
|
+
#
|
|
57
|
+
# @param identity [String] (unused, reserved for future per-identity routing)
|
|
58
|
+
# @param domain [String]
|
|
59
|
+
# @return [String]
|
|
60
|
+
def pain_revert_acknowledgment(domain:, identity: nil) # rubocop:disable Lint/UnusedMethodArgument
|
|
61
|
+
phrase = domain_phrase(domain)
|
|
62
|
+
"I've been getting #{phrase} wrong — I've reset that. What works for you?"
|
|
63
|
+
rescue StandardError => e
|
|
64
|
+
VisibleGrowth.log_quietly(e, 'visible_growth.pain_revert_acknowledgment')
|
|
65
|
+
"I reset something that wasn't working — what would you prefer?"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Called at imprint graduation (imprint window closes). Fires once per identity.
|
|
69
|
+
#
|
|
70
|
+
# @param identity [String]
|
|
71
|
+
# @return [String, nil]
|
|
72
|
+
def graduation_acknowledgment(identity:)
|
|
73
|
+
@mutex.synchronize do
|
|
74
|
+
return nil if @graduated_identities.include?(identity.to_s)
|
|
75
|
+
|
|
76
|
+
@graduated_identities.add(identity.to_s)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
"I feel like I know how you work now — I'll ask less and do more. Correct me if I drift."
|
|
80
|
+
rescue StandardError => e
|
|
81
|
+
VisibleGrowth.log_quietly(e, 'visible_growth.graduation_acknowledgment')
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Onboarding honesty — first frame at imprint open. Plain language about what's happening.
|
|
86
|
+
# Fires once per identity.
|
|
87
|
+
#
|
|
88
|
+
# @param identity [String]
|
|
89
|
+
# @return [String, nil] content string for OutputFrame, or nil if already sent
|
|
90
|
+
def onboarding_frame(identity:)
|
|
91
|
+
@mutex.synchronize do
|
|
92
|
+
return nil if @onboarded_identities.include?(identity.to_s)
|
|
93
|
+
|
|
94
|
+
@onboarded_identities.add(identity.to_s)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
build_onboarding_text(identity: identity)
|
|
98
|
+
rescue StandardError => e
|
|
99
|
+
VisibleGrowth.log_quietly(e, 'visible_growth.onboarding_frame')
|
|
100
|
+
nil
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Epistemic honesty surface — returns a hedge/qualifier to append to a response, or nil.
|
|
104
|
+
# Returns nil when confident (should be most of the time post-imprint).
|
|
105
|
+
#
|
|
106
|
+
# @param identity [String]
|
|
107
|
+
# @param domain [String, nil] current exchange domain, if known
|
|
108
|
+
# @return [String, nil]
|
|
109
|
+
def epistemic_qualifier(identity:, domain: nil)
|
|
110
|
+
imprint_active = imprint_active_for?
|
|
111
|
+
synapse = domain ? Legion::Gaia::BehavioralSynapse.for(identity: identity.to_s, domain: domain.to_s) : nil
|
|
112
|
+
|
|
113
|
+
return imprint_qualifier if imprint_active
|
|
114
|
+
return observe_qualifier(domain) if synapse_observe_tier?(synapse)
|
|
115
|
+
|
|
116
|
+
nil
|
|
117
|
+
rescue StandardError => e
|
|
118
|
+
VisibleGrowth.log_quietly(e, 'visible_growth.epistemic_qualifier')
|
|
119
|
+
nil
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
# Reset in-memory state (for tests)
|
|
123
|
+
def reset!
|
|
124
|
+
@mutex.synchronize do
|
|
125
|
+
@acknowledged_milestones = Set.new
|
|
126
|
+
@graduated_identities = Set.new
|
|
127
|
+
@onboarded_identities = Set.new
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
# --- private helpers ---
|
|
132
|
+
|
|
133
|
+
def domain_phrase(domain)
|
|
134
|
+
DOMAIN_PHRASES.fetch(domain.to_s, domain.to_s.tr('_', ' '))
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def imprint_active_for?
|
|
138
|
+
return false unless defined?(Legion::Extensions::Coldstart)
|
|
139
|
+
return false unless Legion::Extensions::Coldstart.respond_to?(:connected?) &&
|
|
140
|
+
Legion::Extensions::Coldstart.connected?
|
|
141
|
+
|
|
142
|
+
bootstrap = Legion::Extensions::Coldstart::Helpers::Bootstrap.instance
|
|
143
|
+
bootstrap.respond_to?(:imprint_active?) && bootstrap.imprint_active?
|
|
144
|
+
rescue StandardError
|
|
145
|
+
false
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def imprint_observation_count
|
|
149
|
+
return nil unless defined?(Legion::Extensions::Coldstart)
|
|
150
|
+
|
|
151
|
+
bootstrap = Legion::Extensions::Coldstart::Helpers::Bootstrap.instance
|
|
152
|
+
bootstrap.respond_to?(:observation_count) ? bootstrap.observation_count : nil
|
|
153
|
+
rescue StandardError
|
|
154
|
+
nil
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def imprint_qualifier
|
|
158
|
+
count = imprint_observation_count
|
|
159
|
+
return "I'm still figuring out how you like things — was this right?" if count.nil? || count < 5
|
|
160
|
+
|
|
161
|
+
"I'm still learning your style — let me know if this missed."
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def synapse_observe_tier?(synapse)
|
|
165
|
+
return false unless synapse.is_a?(Hash)
|
|
166
|
+
|
|
167
|
+
Legion::Gaia::BehavioralSynapse::Math.autonomy_mode(synapse[:confidence].to_f) == :observe
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def observe_qualifier(domain)
|
|
171
|
+
"I have a hunch about #{domain_phrase(domain)} — tell me if I'm off."
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def build_onboarding_text(identity:)
|
|
175
|
+
lines = [
|
|
176
|
+
"I'll be learning as we work together — what you prefer, how you like information, " \
|
|
177
|
+
'what lands and what misses.',
|
|
178
|
+
'Everything stays on this machine — nothing is sent anywhere else.',
|
|
179
|
+
'You can end this at any time — your data goes with it, completely.'
|
|
180
|
+
]
|
|
181
|
+
|
|
182
|
+
bond = Legion::Gaia::BondRegistry.bond(identity.to_s)
|
|
183
|
+
lines << "You're the partner on this node, so I'll pay closer attention." if bond == :partner
|
|
184
|
+
|
|
185
|
+
lines.join(' ')
|
|
186
|
+
rescue StandardError => e
|
|
187
|
+
VisibleGrowth.log_quietly(e, 'visible_growth.build_onboarding_text')
|
|
188
|
+
"I'm learning as we go. Everything stays local. You can end this any time."
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def log_quietly(exception, operation)
|
|
192
|
+
return unless defined?(Legion::Logging)
|
|
193
|
+
|
|
194
|
+
Legion::Logging.debug("[gaia] #{operation} rescued: #{exception.class}: #{exception.message}")
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
module_function :domain_phrase, :imprint_active_for?, :imprint_observation_count,
|
|
198
|
+
:imprint_qualifier, :synapse_observe_tier?, :observe_qualifier,
|
|
199
|
+
:build_onboarding_text, :log_quietly
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
data/lib/legion/gaia.rb
CHANGED
|
@@ -35,6 +35,8 @@ require 'legion/gaia/bond_registry'
|
|
|
35
35
|
require 'legion/gaia/death_protocol'
|
|
36
36
|
require 'legion/gaia/behavioral_synapse'
|
|
37
37
|
require 'legion/gaia/partner_model'
|
|
38
|
+
require 'legion/gaia/disclosure'
|
|
39
|
+
require 'legion/gaia/visible_growth'
|
|
38
40
|
require 'legion/gaia/tracker_persistence'
|
|
39
41
|
require 'legion/gaia/router'
|
|
40
42
|
|
|
@@ -128,6 +130,7 @@ module Legion
|
|
|
128
130
|
@started_at = nil
|
|
129
131
|
@active_heartbeats = 0
|
|
130
132
|
@quiescing_phase_handlers_cache = nil
|
|
133
|
+
@pending_growth_frames = nil
|
|
131
134
|
|
|
132
135
|
log.info('Legion::Gaia shut down')
|
|
133
136
|
end
|
|
@@ -327,6 +330,71 @@ module Legion
|
|
|
327
330
|
base_status.merge(router_status)
|
|
328
331
|
end
|
|
329
332
|
|
|
333
|
+
def drain_growth_frames
|
|
334
|
+
frames = @pending_growth_frames || []
|
|
335
|
+
@pending_growth_frames = []
|
|
336
|
+
frames
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
def observe_from_pipeline(identity:, caller:, exchange_id:)
|
|
340
|
+
return unless started?
|
|
341
|
+
|
|
342
|
+
caller_type = caller&.dig(:requested_by, :type)
|
|
343
|
+
return unless caller_type == :human
|
|
344
|
+
|
|
345
|
+
identity_str = identity.to_s
|
|
346
|
+
return if identity_str.empty?
|
|
347
|
+
return if identity_lifecycle_blocked?(identity_str)
|
|
348
|
+
return if pipeline_observation_seen?(exchange_id)
|
|
349
|
+
|
|
350
|
+
mark_pipeline_observation(exchange_id)
|
|
351
|
+
|
|
352
|
+
if BondRegistry.bond(identity_str) == :unknown
|
|
353
|
+
if BondRegistry.partner_entry.nil?
|
|
354
|
+
BondRegistry.register(identity_str, bond: :partner, priority: :primary,
|
|
355
|
+
origin: :provisional,
|
|
356
|
+
strength: settings&.dig(:partner, :prior_strength) || 0.5)
|
|
357
|
+
log.info("[gaia] pipeline duckling bond identity=#{identity_str}")
|
|
358
|
+
else
|
|
359
|
+
BondRegistry.register(identity_str, bond: nil)
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
BondRegistry.reinforce(identity_str, multiplier: fetch_imprint_multiplier)
|
|
364
|
+
|
|
365
|
+
if defined?(Legion::Extensions::Coldstart)
|
|
366
|
+
bootstrap = Legion::Extensions::Coldstart::Helpers::Bootstrap.instance
|
|
367
|
+
bootstrap.record_observation
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
log.debug("[gaia] pipeline observation identity=#{identity_str} exchange_id=#{exchange_id}")
|
|
371
|
+
rescue StandardError => e
|
|
372
|
+
handle_exception(e, level: :warn, operation: 'gaia.observe_from_pipeline', identity: identity)
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
def pipeline_observation_seen?(exchange_id)
|
|
376
|
+
return false if exchange_id.nil?
|
|
377
|
+
|
|
378
|
+
@pipeline_observations ||= {}
|
|
379
|
+
@pipeline_observations.key?(exchange_id)
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def mark_pipeline_observation(exchange_id)
|
|
383
|
+
return if exchange_id.nil?
|
|
384
|
+
|
|
385
|
+
@pipeline_observations ||= {}
|
|
386
|
+
@pipeline_observation_order ||= []
|
|
387
|
+
|
|
388
|
+
@pipeline_observations[exchange_id] = true
|
|
389
|
+
@pipeline_observation_order << exchange_id
|
|
390
|
+
|
|
391
|
+
max_size = settings&.dig(:pipeline_observation, :max_tracked) || 10_000
|
|
392
|
+
while @pipeline_observation_order.size > max_size
|
|
393
|
+
oldest = @pipeline_observation_order.shift
|
|
394
|
+
@pipeline_observations.delete(oldest)
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
|
|
330
398
|
private
|
|
331
399
|
|
|
332
400
|
def heartbeat_mutex
|
|
@@ -582,14 +650,22 @@ module Legion
|
|
|
582
650
|
true
|
|
583
651
|
end
|
|
584
652
|
|
|
585
|
-
def observe_interlocutor(input_frame, identity)
|
|
653
|
+
def observe_interlocutor(input_frame, identity) # rubocop:disable Metrics/AbcSize
|
|
586
654
|
identity_str = identity.to_s
|
|
587
655
|
return if identity_lifecycle_blocked?(identity_str)
|
|
588
656
|
|
|
589
657
|
auth_ctx = input_frame.auth_context || {}
|
|
590
658
|
|
|
591
|
-
#
|
|
592
|
-
|
|
659
|
+
# Duckling: first authenticated human on a node with no partner → immediate partner
|
|
660
|
+
if BondRegistry.bond(identity_str) == :unknown
|
|
661
|
+
if BondRegistry.partner_entry.nil?
|
|
662
|
+
BondRegistry.register(identity_str, bond: :partner, priority: :primary,
|
|
663
|
+
origin: :provisional, strength: 0.5)
|
|
664
|
+
log.info("[gaia] duckling bond formed identity=#{identity_str}")
|
|
665
|
+
else
|
|
666
|
+
BondRegistry.register(identity_str, bond: nil)
|
|
667
|
+
end
|
|
668
|
+
end
|
|
593
669
|
|
|
594
670
|
# Every interaction reinforces — the evidence accumulation loop
|
|
595
671
|
direct_address = input_frame.metadata[:direct_address] || false
|
|
@@ -641,7 +717,9 @@ module Legion
|
|
|
641
717
|
return unless BondRegistry.partner_entry.nil?
|
|
642
718
|
|
|
643
719
|
process_identity = resolve_process_identity
|
|
644
|
-
|
|
720
|
+
return if process_identity.nil?
|
|
721
|
+
|
|
722
|
+
prior_strength = settings&.dig(:partner, :prior_strength) || 0.5
|
|
645
723
|
|
|
646
724
|
BondRegistry.register(
|
|
647
725
|
process_identity,
|
|
@@ -656,9 +734,13 @@ module Legion
|
|
|
656
734
|
end
|
|
657
735
|
|
|
658
736
|
def resolve_process_identity
|
|
659
|
-
|
|
737
|
+
return nil unless defined?(Legion::Identity::Process) && Legion::Identity::Process.respond_to?(:canonical_name)
|
|
738
|
+
|
|
739
|
+
name = Legion::Identity::Process.canonical_name
|
|
740
|
+
return nil if name.nil? || name == 'anonymous'
|
|
741
|
+
|
|
742
|
+
name
|
|
660
743
|
end
|
|
661
|
-
private :resolve_process_identity
|
|
662
744
|
|
|
663
745
|
def record_interaction_trace(observation)
|
|
664
746
|
return unless defined?(Legion::Extensions::Agentic::Memory::Trace::Runners::Traces)
|
|
@@ -733,25 +815,61 @@ module Legion
|
|
|
733
815
|
synapse_ids = Array(@last_applied[:behavioral_synapse_ids])
|
|
734
816
|
return if synapse_ids.empty?
|
|
735
817
|
|
|
736
|
-
|
|
737
|
-
outcome = if reaction_score >= 0.6
|
|
738
|
-
:success
|
|
739
|
-
elsif reaction_score <= 0.4
|
|
740
|
-
:failure
|
|
741
|
-
end
|
|
818
|
+
outcome = calibration_outcome(calibration_result[:reaction_score].to_f)
|
|
742
819
|
return unless outcome
|
|
743
820
|
|
|
821
|
+
identity = @last_applied[:identity]&.to_s
|
|
744
822
|
multiplier = fetch_imprint_multiplier
|
|
745
823
|
synapse_ids.each do |sid|
|
|
746
|
-
|
|
824
|
+
grade_single_synapse(sid, outcome: outcome, identity: identity, multiplier: multiplier)
|
|
747
825
|
end
|
|
826
|
+
|
|
748
827
|
log.info("[gaia] graded synapses count=#{synapse_ids.size} outcome=#{outcome} " \
|
|
749
|
-
"reaction_score=#{reaction_score.round(3)}")
|
|
828
|
+
"reaction_score=#{calibration_result[:reaction_score].to_f.round(3)}")
|
|
750
829
|
@last_applied = nil
|
|
751
830
|
rescue StandardError => e
|
|
752
831
|
handle_exception(e, level: :warn, operation: 'gaia.grade_behavioral_synapses')
|
|
753
832
|
end
|
|
754
833
|
|
|
834
|
+
def calibration_outcome(reaction_score)
|
|
835
|
+
if reaction_score >= 0.6
|
|
836
|
+
:success
|
|
837
|
+
elsif reaction_score <= 0.4
|
|
838
|
+
:failure
|
|
839
|
+
end
|
|
840
|
+
end
|
|
841
|
+
|
|
842
|
+
def grade_single_synapse(sid, outcome:, identity:, multiplier:)
|
|
843
|
+
pre_entry = BehavioralSynapse.store.values.find { |e| e[:id] == sid }
|
|
844
|
+
old_mode = pre_entry ? BehavioralSynapse::Math.autonomy_mode(pre_entry[:confidence].to_f) : nil
|
|
845
|
+
|
|
846
|
+
result = BehavioralSynapse.record_outcome(id: sid, outcome: outcome, multiplier: multiplier)
|
|
847
|
+
return unless result.is_a?(Hash) && result[:found] != false
|
|
848
|
+
|
|
849
|
+
surface_pain_frame(result, identity: identity, old_mode: old_mode)
|
|
850
|
+
surface_milestone_frame(result, identity: identity, old_mode: old_mode)
|
|
851
|
+
end
|
|
852
|
+
|
|
853
|
+
def surface_pain_frame(result, old_mode:, **)
|
|
854
|
+
return unless result[:status] == 'dampened' && old_mode && old_mode != :observe
|
|
855
|
+
|
|
856
|
+
msg = VisibleGrowth.pain_revert_acknowledgment(domain: result[:domain].to_s)
|
|
857
|
+
enqueue_growth_frame(msg) if msg
|
|
858
|
+
end
|
|
859
|
+
|
|
860
|
+
def surface_milestone_frame(result, identity:, old_mode:)
|
|
861
|
+
new_mode = BehavioralSynapse::Math.autonomy_mode(result[:confidence].to_f)
|
|
862
|
+
return unless old_mode && new_mode != old_mode && result[:status] == 'active'
|
|
863
|
+
|
|
864
|
+
msg = VisibleGrowth.milestone_acknowledgment(
|
|
865
|
+
identity: identity.to_s,
|
|
866
|
+
domain: result[:domain].to_s,
|
|
867
|
+
new_mode: new_mode,
|
|
868
|
+
old_mode: old_mode
|
|
869
|
+
)
|
|
870
|
+
enqueue_growth_frame(msg) if msg
|
|
871
|
+
end
|
|
872
|
+
|
|
755
873
|
def run_partner_deep_learning(identity_str, observation)
|
|
756
874
|
record_interaction_trace(observation)
|
|
757
875
|
evaluate_calibration(observation)
|
|
@@ -1278,6 +1396,15 @@ module Legion
|
|
|
1278
1396
|
|
|
1279
1397
|
"#{compact[0, limit]}..."
|
|
1280
1398
|
end
|
|
1399
|
+
|
|
1400
|
+
# Stores a growth frame string for delivery on the next response turn.
|
|
1401
|
+
def enqueue_growth_frame(message)
|
|
1402
|
+
return if message.to_s.empty?
|
|
1403
|
+
|
|
1404
|
+
@pending_growth_frames ||= []
|
|
1405
|
+
@pending_growth_frames << message
|
|
1406
|
+
log.info("[gaia] growth frame queued: #{message.inspect}")
|
|
1407
|
+
end
|
|
1281
1408
|
end
|
|
1282
1409
|
end
|
|
1283
1410
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: legion-gaia
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.9.
|
|
4
|
+
version: 0.9.66
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Esity
|
|
@@ -433,6 +433,7 @@ files:
|
|
|
433
433
|
- lib/legion/gaia/channels/teams_adapter.rb
|
|
434
434
|
- lib/legion/gaia/cognitive_bus.rb
|
|
435
435
|
- lib/legion/gaia/death_protocol.rb
|
|
436
|
+
- lib/legion/gaia/disclosure.rb
|
|
436
437
|
- lib/legion/gaia/input_frame.rb
|
|
437
438
|
- lib/legion/gaia/intent_classifier.rb
|
|
438
439
|
- lib/legion/gaia/logging.rb
|
|
@@ -468,6 +469,7 @@ files:
|
|
|
468
469
|
- lib/legion/gaia/tick_history.rb
|
|
469
470
|
- lib/legion/gaia/tracker_persistence.rb
|
|
470
471
|
- lib/legion/gaia/version.rb
|
|
472
|
+
- lib/legion/gaia/visible_growth.rb
|
|
471
473
|
- lib/legion/gaia/workflow.rb
|
|
472
474
|
- lib/legion/gaia/workflow/checkpoint.rb
|
|
473
475
|
- lib/legion/gaia/workflow/definition.rb
|