Skip to content

Commit 251a95b

Browse files
authored
Merge pull request #141 from AdaWorldAPI/claude/vsaclip-hamming-recognition-y0b94
test: A2A feedback bridge — multi-agent awareness convergence via cha…
2 parents a444b37 + ee3c56f commit 251a95b

1 file changed

Lines changed: 375 additions & 0 deletions

File tree

src/learning/feedback.rs

Lines changed: 375 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -768,4 +768,379 @@ mod tests {
768768
);
769769
assert!(signal2.decay_rate < 0.90, "high crystallization should decrease decay: {}", signal2.decay_rate);
770770
}
771+
772+
#[test]
773+
fn test_multi_agent_awareness_convergence_no_a2a() {
774+
// Core awareness convergence test — no A2A dependency.
775+
// Simulates the feedback loop that runs when rustynum GEMM backend
776+
// produces hybrid pipeline results and ladybug-rs applies them.
777+
//
778+
// 3 "agents" each produce feedback signals with different characteristics:
779+
// - Explorer: high crystallization (settled knowledge)
780+
// - Contrarian: high sign flips (polarity conflict)
781+
// - Noisy: mostly mantissa noise (no real signal)
782+
//
783+
// A learner's WideMetaView accumulates feedback from all agents.
784+
// After multiple rounds, the NARS truth values, hybrid weights,
785+
// and RL Q-values should reflect the combined awareness.
786+
let mut words = [0u64; 256];
787+
788+
// Initialize: unknown state (f=0.5, c=0.1)
789+
let init_f = 0.5f32;
790+
let init_c = 0.1f32;
791+
words[4] = init_f.to_bits() as u64 | ((init_c.to_bits() as u64) << 32);
792+
793+
// Round 1: Explorer — crystallized, Revision signal
794+
let explorer = build_feedback(
795+
500, 16384, 500, 8000, 8000, 7500,
796+
2, 10, 100, 1024, 0.90, 0.03,
797+
&[0.85; 32], 0.95,
798+
);
799+
assert_eq!(explorer.bf16_causal.inferred_op, NarsInferenceType::Revision);
800+
apply_feedback(&mut words, &explorer, 0.2, 1);
801+
802+
let freq_r1 = f32::from_bits((words[4] & 0xFFFF_FFFF) as u32);
803+
assert!(freq_r1 > init_f, "Revision should increase freq: {} > {}", freq_r1, init_f);
804+
805+
// Round 2: Contrarian — negation signal
806+
let contrarian = build_feedback(
807+
5000, 16384, 5000, 8000, 8000, 3000,
808+
400, 50, 100, 1024, 0.10, 0.45,
809+
&[0.3; 32], 0.95,
810+
);
811+
assert_eq!(contrarian.bf16_causal.inferred_op, NarsInferenceType::Negation);
812+
apply_feedback(&mut words, &contrarian, 0.2, 2);
813+
814+
let freq_r2 = f32::from_bits((words[4] & 0xFFFF_FFFF) as u32);
815+
// Negation should moderate the frequency
816+
assert!(freq_r2 > 0.0 && freq_r2 <= 1.0, "Freq should be valid: {}", freq_r2);
817+
818+
// Round 3: Explorer again — reinforces crystallized view
819+
let explorer2 = build_feedback(
820+
300, 16384, 300, 8200, 8100, 7800,
821+
1, 5, 80, 1024, 0.92, 0.02,
822+
&[0.9; 32], 0.94,
823+
);
824+
apply_feedback(&mut words, &explorer2, 0.2, 3);
825+
826+
let freq_r3 = f32::from_bits((words[4] & 0xFFFF_FFFF) as u32);
827+
let conf_r3 = f32::from_bits(((words[4] >> 32) & 0xFFFF_FFFF) as u32);
828+
829+
// After 3 rounds, confidence should have accumulated
830+
assert!(conf_r3 > init_c, "Confidence should grow: {} > {}", conf_r3, init_c);
831+
832+
// Hybrid weights should have been written (W144-W159)
833+
let hw0 = f32::from_bits((words[144] & 0xFFFF_FFFF) as u32);
834+
let hw1 = f32::from_bits(((words[144] >> 32) & 0xFFFF_FFFF) as u32);
835+
assert!(hw0 > 0.0 && hw1 > 0.0, "Hybrid weights should be positive: w0={}, w1={}", hw0, hw1);
836+
837+
// Evidence history should have 3 entries shifted through (W162)
838+
let ev0 = f32::from_bits((words[162] & 0xFFFF_FFFF) as u32);
839+
assert!(ev0 > 0.0, "Evidence history should have entries: {}", ev0);
840+
841+
// RL Q-values: action 0 (Revision) should be positive, action 1 (Negation) should exist
842+
let q_revision = f32::from_bits((words[32] & 0xFFFF_FFFF) as u32);
843+
let q_negation = f32::from_bits(((words[32] >> 32) & 0xFFFF_FFFF) as u32);
844+
assert!(q_revision != 0.0 || q_negation != 0.0,
845+
"RL Q-values should be updated: revision={}, negation={}", q_revision, q_negation);
846+
847+
// Decay rate should have adapted: contrarian increased it, explorer decreased it
848+
let decay = f32::from_bits(((words[160] >> 32) & 0xFFFF_FFFF) as u32);
849+
assert!(
850+
decay > 0.0 && decay < 1.0,
851+
"Decay rate should be valid: {}",
852+
decay
853+
);
854+
}
855+
856+
// ====================================================================
857+
// Multi-agent A2A awareness feedback bridge (requires crewai feature)
858+
// ====================================================================
859+
//
860+
// Demonstrates the full loop:
861+
//
862+
// Agent A: recognition → build_feedback() → encode as A2A message
863+
// ↓ A2A channel (XOR superposition in BindSpace 0x0F)
864+
// Agent B: read channel → decode feedback → apply_feedback() → WideMetaView
865+
//
866+
// When compiled into a single binary with rustynum, the recognition
867+
// results come from the GEMM-backed hybrid pipeline. Here we simulate
868+
// the numbers that would flow from hybrid_pipeline() + extract_learning_signal().
869+
870+
#[test]
871+
#[cfg(feature = "crewai")]
872+
fn test_a2a_feedback_bridge_two_agents() {
873+
use crate::storage::bind_space::BindSpace;
874+
use crate::orchestration::a2a::{
875+
A2AProtocol, A2AMessage, MessageKind, DeliveryStatus,
876+
};
877+
878+
let mut space = BindSpace::new();
879+
let mut protocol = A2AProtocol::new();
880+
881+
// Agent A (slot 0x02): explorer agent — produces crystallized feedback
882+
// Agent B (slot 0x05): learner agent — receives and applies feedback
883+
let _channel = protocol.open_channel(0x02, 0x05);
884+
885+
// === Agent A's recognition cycle ===
886+
//
887+
// These numbers would come from rustynum hybrid_pipeline():
888+
// - hamming_distance, total_bits from K2 exact
889+
// - conflict/energy/agreement from EnergyConflict
890+
// - sign_flips etc. from BF16 structural diff
891+
// - crystallized/tension from extract_learning_signal()
892+
let agent_a_feedback = build_feedback(
893+
800, // hamming_distance (close match)
894+
16384, // total_bits (SKU-16K)
895+
800, // conflict
896+
8100, // energy_a
897+
8050, // energy_b
898+
7250, // agreement (high — good match)
899+
3, // sign_flips (few)
900+
15, // exponent_bits_changed
901+
120, // mantissa_bits_changed
902+
1024, // total_bf16_dims
903+
0.85, // crystallized_ratio (high — settled)
904+
0.04, // tension_ratio (low)
905+
&[0.8; 32], // attention_weights from awareness
906+
0.95, // current_decay
907+
);
908+
909+
// Verify Agent A's feedback is a revision signal (crystallized)
910+
assert_eq!(
911+
agent_a_feedback.bf16_causal.inferred_op,
912+
NarsInferenceType::Revision,
913+
"High crystallization should trigger Revision"
914+
);
915+
916+
// === Agent A sends feedback through A2A channel ===
917+
//
918+
// In production, the feedback signal's hybrid_weights would be
919+
// encoded into the message payload or fingerprint. Here we use
920+
// the Knowledge message kind to carry awareness.
921+
let feedback_msg = A2AMessage {
922+
id: "feedback-001".to_string(),
923+
sender_slot: 0x02,
924+
receiver_slot: 0x05,
925+
kind: MessageKind::Knowledge,
926+
payload: format!(
927+
"crystallized={:.2},tension={:.2},op={:?},decay={:.3}",
928+
agent_a_feedback.bf16_causal.crystallized_ratio,
929+
agent_a_feedback.bf16_causal.tension_ratio,
930+
agent_a_feedback.bf16_causal.inferred_op,
931+
agent_a_feedback.decay_rate,
932+
),
933+
fingerprint: None,
934+
timestamp: 1,
935+
status: DeliveryStatus::Pending,
936+
thinking_style_hint: Some("analytical".to_string()),
937+
resonance_weight: 1.0,
938+
};
939+
940+
let status = protocol.send(feedback_msg, &mut space);
941+
assert_eq!(status, DeliveryStatus::Delivered);
942+
943+
// Channel should show activity
944+
let resonance = protocol.field_resonance(0x02, 0x05);
945+
assert!(
946+
resonance > 0.0,
947+
"A2A channel should have resonance after feedback: {}",
948+
resonance
949+
);
950+
assert_eq!(protocol.superposition_depth(0x02, 0x05), 1);
951+
952+
// === Agent B reads the channel and applies feedback ===
953+
//
954+
// In production, Agent B would decode the message and reconstruct
955+
// the FeedbackSignal. Here we apply it directly to a WideMetaView buffer.
956+
let mut words_b = [0u64; 256];
957+
958+
// Initialize Agent B's NARS state (unknown: f=0.5, c=0.1)
959+
let init_freq = 0.5f32;
960+
let init_conf = 0.1f32;
961+
words_b[4] = init_freq.to_bits() as u64 | ((init_conf.to_bits() as u64) << 32);
962+
963+
// Apply Agent A's feedback to Agent B's metadata
964+
apply_feedback(&mut words_b, &agent_a_feedback, 0.2, 1);
965+
966+
// Verify Agent B's NARS state was updated
967+
let new_freq = f32::from_bits((words_b[4] & 0xFFFF_FFFF) as u32);
968+
let new_conf = f32::from_bits(((words_b[4] >> 32) & 0xFFFF_FFFF) as u32);
969+
assert!(
970+
new_freq > 0.5,
971+
"Revision from Agent A should increase frequency: {}",
972+
new_freq
973+
);
974+
assert!(
975+
new_conf > 0.0 && new_conf <= 1.0,
976+
"Confidence should be valid: {}",
977+
new_conf
978+
);
979+
980+
// Verify hybrid weights were written to W144-W159
981+
let w0 = f32::from_bits((words_b[144] & 0xFFFF_FFFF) as u32);
982+
assert!(
983+
w0 > 0.0,
984+
"Hybrid weight W144 should be non-zero after feedback: {}",
985+
w0
986+
);
987+
988+
// Verify RL Q-value was updated (action slot 0 = Revision)
989+
let q0 = f32::from_bits((words_b[32] & 0xFFFF_FFFF) as u32);
990+
assert!(
991+
q0 != 0.0,
992+
"Q-value for Revision action should be updated: {}",
993+
q0
994+
);
995+
}
996+
997+
#[test]
998+
#[cfg(feature = "crewai")]
999+
fn test_a2a_multi_round_feedback_convergence() {
1000+
use crate::storage::bind_space::BindSpace;
1001+
use crate::orchestration::a2a::{
1002+
A2AProtocol, A2AMessage, MessageKind, DeliveryStatus,
1003+
};
1004+
1005+
let mut space = BindSpace::new();
1006+
let mut protocol = A2AProtocol::new();
1007+
1008+
// 3 agents: explorer(0), contrarian(1), learner(2)
1009+
// explorer → learner: crystallized feedback
1010+
// contrarian → learner: negation feedback
1011+
let _ch_explore = protocol.open_channel(0, 2);
1012+
let _ch_contra = protocol.open_channel(1, 2);
1013+
1014+
let mut learner_words = [0u64; 256];
1015+
let init_freq = 0.5f32;
1016+
let init_conf = 0.1f32;
1017+
learner_words[4] = init_freq.to_bits() as u64 | ((init_conf.to_bits() as u64) << 32);
1018+
1019+
// === Round 1: Explorer sends crystallized feedback ===
1020+
let explorer_feedback = build_feedback(
1021+
500, 16384, 500, 8000, 8000, 7500,
1022+
2, 10, 100, 1024, 0.90, 0.03,
1023+
&[0.85; 32], 0.95,
1024+
);
1025+
let msg_explore = A2AMessage {
1026+
id: "explore-r1".to_string(),
1027+
sender_slot: 0,
1028+
receiver_slot: 2,
1029+
kind: MessageKind::Knowledge,
1030+
payload: "crystallized_feedback".to_string(),
1031+
fingerprint: None,
1032+
timestamp: 1,
1033+
status: DeliveryStatus::Pending,
1034+
thinking_style_hint: None,
1035+
resonance_weight: 1.0,
1036+
};
1037+
protocol.send(msg_explore, &mut space);
1038+
apply_feedback(&mut learner_words, &explorer_feedback, 0.15, 1);
1039+
1040+
let freq_after_r1 = f32::from_bits((learner_words[4] & 0xFFFF_FFFF) as u32);
1041+
1042+
// === Round 2: Contrarian sends negation feedback ===
1043+
let contra_feedback = build_feedback(
1044+
5000, 16384, 5000, 8000, 8000, 3000,
1045+
400, 50, 100, 1024, 0.10, 0.45,
1046+
&[0.3; 32], 0.95,
1047+
);
1048+
let msg_contra = A2AMessage {
1049+
id: "contra-r2".to_string(),
1050+
sender_slot: 1,
1051+
receiver_slot: 2,
1052+
kind: MessageKind::Knowledge,
1053+
payload: "negation_feedback".to_string(),
1054+
fingerprint: None,
1055+
timestamp: 2,
1056+
status: DeliveryStatus::Pending,
1057+
thinking_style_hint: None,
1058+
resonance_weight: 1.0,
1059+
};
1060+
protocol.send(msg_contra, &mut space);
1061+
apply_feedback(&mut learner_words, &contra_feedback, 0.15, 2);
1062+
1063+
let freq_after_r2 = f32::from_bits((learner_words[4] & 0xFFFF_FFFF) as u32);
1064+
1065+
// === Round 3: Explorer sends another crystallized signal ===
1066+
let explorer_feedback_r3 = build_feedback(
1067+
300, 16384, 300, 8200, 8100, 7800,
1068+
1, 5, 80, 1024, 0.92, 0.02,
1069+
&[0.9; 32], 0.94,
1070+
);
1071+
let msg_explore_r3 = A2AMessage {
1072+
id: "explore-r3".to_string(),
1073+
sender_slot: 0,
1074+
receiver_slot: 2,
1075+
kind: MessageKind::Knowledge,
1076+
payload: "crystallized_feedback_r3".to_string(),
1077+
fingerprint: None,
1078+
timestamp: 3,
1079+
status: DeliveryStatus::Pending,
1080+
thinking_style_hint: None,
1081+
resonance_weight: 1.0,
1082+
};
1083+
protocol.send(msg_explore_r3, &mut space);
1084+
apply_feedback(&mut learner_words, &explorer_feedback_r3, 0.15, 3);
1085+
1086+
let freq_after_r3 = f32::from_bits((learner_words[4] & 0xFFFF_FFFF) as u32);
1087+
1088+
// === Verify convergence ===
1089+
1090+
// After contrarian negation, frequency should have been pushed down
1091+
assert!(
1092+
freq_after_r2 < freq_after_r1 || freq_after_r2 < 0.8,
1093+
"Negation should reduce or moderate frequency: r1={:.3} r2={:.3}",
1094+
freq_after_r1, freq_after_r2
1095+
);
1096+
1097+
// After 2nd explorer signal, frequency should recover
1098+
assert!(
1099+
freq_after_r3 > 0.0 && freq_after_r3 <= 1.0,
1100+
"Final frequency should be valid: {}",
1101+
freq_after_r3
1102+
);
1103+
1104+
// A2A channel awareness should accumulate
1105+
let explore_awareness = protocol.field_resonance(0, 2);
1106+
let contra_awareness = protocol.field_resonance(1, 2);
1107+
let total = protocol.total_awareness();
1108+
1109+
assert!(
1110+
explore_awareness > 0.0,
1111+
"Explorer channel should have resonance: {}",
1112+
explore_awareness
1113+
);
1114+
assert!(
1115+
contra_awareness > 0.0,
1116+
"Contrarian channel should have resonance: {}",
1117+
contra_awareness
1118+
);
1119+
assert!(
1120+
total > explore_awareness,
1121+
"Total awareness should exceed single channel: {}",
1122+
total
1123+
);
1124+
1125+
// Explorer channel had 2 messages, should have higher superposition depth
1126+
assert_eq!(protocol.superposition_depth(0, 2), 2);
1127+
assert_eq!(protocol.superposition_depth(1, 2), 1);
1128+
1129+
// Hybrid weights should reflect mixed signals
1130+
let w0 = f32::from_bits((learner_words[144] & 0xFFFF_FFFF) as u32);
1131+
let w1 = f32::from_bits(((learner_words[144] >> 32) & 0xFFFF_FFFF) as u32);
1132+
assert!(
1133+
w0 > 0.0 && w1 > 0.0,
1134+
"Hybrid weights should be positive: w0={} w1={}",
1135+
w0, w1
1136+
);
1137+
1138+
// Evidence history should have entries (W162)
1139+
let ev_slot0 = f32::from_bits((learner_words[162] & 0xFFFF_FFFF) as u32);
1140+
assert!(
1141+
ev_slot0 > 0.0,
1142+
"Evidence history should have entries: slot0={}",
1143+
ev_slot0
1144+
);
1145+
}
7711146
}

0 commit comments

Comments
 (0)