use std::collections::HashMap;
use std::time::Duration;
use serde::{Deserialize, Serialize};
use super::memory::{
AgentMemory, CodePattern, DeadEnd, MemoryBackend, MemoryConfig, ModificationRecord, Outcome,
PerformanceBaseline,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
struct ModificationRecordSerde {
id: String,
description: String,
affected_files: Vec<String>,
outcome: String,
metric_deltas: HashMap<String, f64>,
notes: String,
timestamp_ms: u64,
}
impl From<ModificationRecord> for ModificationRecordSerde {
fn from(r: ModificationRecord) -> Self {
Self {
id: r.id,
description: r.description,
affected_files: r.affected_files,
outcome: r.outcome.to_string(),
metric_deltas: r.metric_deltas,
notes: r.notes,
timestamp_ms: r.timestamp_ms,
}
}
}
impl TryFrom<ModificationRecordSerde> for ModificationRecord {
type Error = String;
fn try_from(s: ModificationRecordSerde) -> Result<Self, Self::Error> {
let outcome = match s.outcome.as_str() {
"success" => Outcome::Success,
"failure" => Outcome::Failure,
"rolled_back" => Outcome::RolledBack,
"pending" => Outcome::Pending,
other => return Err(format!("unknown outcome: {}", other)),
};
Ok(ModificationRecord {
id: s.id,
description: s.description,
affected_files: s.affected_files,
outcome,
metric_deltas: s.metric_deltas,
notes: s.notes,
timestamp_ms: s.timestamp_ms,
})
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct CodePatternSerde {
id: String,
title: String,
content: String,
tags: Vec<String>,
use_count: u32,
created_at_ms: u64,
}
impl From<CodePattern> for CodePatternSerde {
fn from(p: CodePattern) -> Self {
let mut tags: Vec<String> = p.tags.into_iter().collect();
tags.sort_unstable();
Self {
id: p.id,
title: p.title,
content: p.content,
tags,
use_count: p.use_count,
created_at_ms: p.created_at_ms,
}
}
}
impl From<CodePatternSerde> for CodePattern {
fn from(s: CodePatternSerde) -> Self {
CodePattern {
id: s.id,
title: s.title,
content: s.content,
tags: s.tags.into_iter().collect(),
use_count: s.use_count,
created_at_ms: s.created_at_ms,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct PerformanceBaselineSerde {
metric: String,
p50: f64,
p95: f64,
p99: f64,
mean: f64,
std_dev: f64,
sample_count: u64,
updated_at_ms: u64,
}
impl From<PerformanceBaseline> for PerformanceBaselineSerde {
fn from(b: PerformanceBaseline) -> Self {
Self {
metric: b.metric,
p50: b.p50,
p95: b.p95,
p99: b.p99,
mean: b.mean,
std_dev: b.std_dev,
sample_count: b.sample_count,
updated_at_ms: b.updated_at_ms,
}
}
}
impl From<PerformanceBaselineSerde> for PerformanceBaseline {
fn from(s: PerformanceBaselineSerde) -> Self {
PerformanceBaseline {
metric: s.metric,
p50: s.p50,
p95: s.p95,
p99: s.p99,
mean: s.mean,
std_dev: s.std_dev,
sample_count: s.sample_count,
updated_at_ms: s.updated_at_ms,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
struct DeadEndSerde {
key: String,
description: String,
reason: String,
related_signals: Vec<String>,
recorded_at_ms: u64,
}
impl From<DeadEnd> for DeadEndSerde {
fn from(d: DeadEnd) -> Self {
Self {
key: d.key,
description: d.description,
reason: d.reason,
related_signals: d.related_signals,
recorded_at_ms: d.recorded_at_ms,
}
}
}
impl From<DeadEndSerde> for DeadEnd {
fn from(s: DeadEndSerde) -> Self {
DeadEnd {
key: s.key,
description: s.description,
reason: s.reason,
related_signals: s.related_signals,
recorded_at_ms: s.recorded_at_ms,
}
}
}
const KEY_MODS: &str = "helix:mem:mods";
const KEY_PATTERNS: &str = "helix:mem:patterns";
const KEY_BASELINES: &str = "helix:mem:baselines";
const KEY_DEADEND_SET: &str = "helix:mem:deadends";
const KEY_DEADEND_DATA: &str = "helix:mem:deadend_data";
#[allow(dead_code)]
pub(crate) trait RedisOps: Send + Sync {
fn lpush(&mut self, key: &str, value: &str) -> Result<(), String>;
fn ltrim(&mut self, key: &str, start: isize, stop: isize) -> Result<(), String>;
fn lrange(&mut self, key: &str, start: isize, stop: isize) -> Result<Vec<String>, String>;
fn hset(&mut self, key: &str, field: &str, value: &str) -> Result<(), String>;
fn hget(&mut self, key: &str, field: &str) -> Result<Option<String>, String>;
fn sadd(&mut self, key: &str, member: &str) -> Result<(), String>;
fn sismember(&mut self, key: &str, member: &str) -> Result<bool, String>;
fn ping(&mut self) -> Result<(), String>;
}
#[cfg(feature = "redis-backing")]
impl RedisOps for redis::Connection {
fn lpush(&mut self, key: &str, value: &str) -> Result<(), String> {
redis::cmd("LPUSH")
.arg(key)
.arg(value)
.query::<()>(self)
.map_err(|e| e.to_string())
}
fn ltrim(&mut self, key: &str, start: isize, stop: isize) -> Result<(), String> {
redis::cmd("LTRIM")
.arg(key)
.arg(start)
.arg(stop)
.query::<()>(self)
.map_err(|e| e.to_string())
}
fn lrange(&mut self, key: &str, start: isize, stop: isize) -> Result<Vec<String>, String> {
redis::cmd("LRANGE")
.arg(key)
.arg(start)
.arg(stop)
.query::<Vec<String>>(self)
.map_err(|e| e.to_string())
}
fn hset(&mut self, key: &str, field: &str, value: &str) -> Result<(), String> {
redis::cmd("HSET")
.arg(key)
.arg(field)
.arg(value)
.query::<()>(self)
.map_err(|e| e.to_string())
}
fn hget(&mut self, key: &str, field: &str) -> Result<Option<String>, String> {
redis::cmd("HGET")
.arg(key)
.arg(field)
.query::<Option<String>>(self)
.map_err(|e| e.to_string())
}
fn sadd(&mut self, key: &str, member: &str) -> Result<(), String> {
redis::cmd("SADD")
.arg(key)
.arg(member)
.query::<()>(self)
.map_err(|e| e.to_string())
}
fn sismember(&mut self, key: &str, member: &str) -> Result<bool, String> {
redis::cmd("SISMEMBER")
.arg(key)
.arg(member)
.query::<bool>(self)
.map_err(|e| e.to_string())
}
fn ping(&mut self) -> Result<(), String> {
redis::cmd("PING")
.query::<String>(self)
.map(|_| ())
.map_err(|e| e.to_string())
}
}
pub struct RedisMemoryBackend {
ops: Box<dyn RedisOps>,
memory: AgentMemory,
max_modifications: usize,
}
impl RedisMemoryBackend {
pub fn connect(url: &str, config: MemoryConfig) -> Result<Self, String> {
let client = redis::Client::open(url).map_err(|e| e.to_string())?;
let mut conn = client
.get_connection()
.map_err(|e| format!("Redis connection failed: {}", e))?;
conn.ping()
.map_err(|e| format!("Redis ping failed: {}", e))?;
let max_modifications = config.max_modifications;
Ok(Self {
ops: Box::new(conn),
memory: AgentMemory::new(config),
max_modifications,
})
}
#[allow(dead_code)]
pub(crate) fn with_ops(ops: Box<dyn RedisOps>, config: MemoryConfig) -> Self {
let max_modifications = config.max_modifications;
Self {
ops,
memory: AgentMemory::new(config),
max_modifications,
}
}
pub fn restore(&mut self) -> Result<usize, String> {
let raw_mods = self.ops.lrange(KEY_MODS, 0, -1)?;
let mut loaded = 0usize;
for json in raw_mods.iter().rev() {
if let Ok(s) = serde_json::from_str::<ModificationRecordSerde>(json) {
if let Ok(record) = ModificationRecord::try_from(s) {
self.memory.record_modification(record);
loaded += 1;
}
}
}
Ok(loaded)
}
}
impl MemoryBackend for RedisMemoryBackend {
fn record_modification(&mut self, record: ModificationRecord) -> String {
let id = self.memory.record_modification(record.clone());
let serde_rec = ModificationRecordSerde::from(record);
if let Ok(json) = serde_json::to_string(&serde_rec) {
let _ = self.ops.lpush(KEY_MODS, &json);
let stop = (self.max_modifications as isize) - 1;
let _ = self.ops.ltrim(KEY_MODS, 0, stop);
}
id
}
fn get_modification(&self, id: &str) -> Option<ModificationRecord> {
self.memory.get_modification(id).cloned()
}
fn recent_modifications(&self, n: usize) -> Vec<ModificationRecord> {
self.memory
.recent_modifications(n)
.into_iter()
.cloned()
.collect()
}
fn success_rate(&self) -> Option<f64> {
let mods: Vec<_> = self.memory.modifications().collect();
if mods.is_empty() {
return None;
}
let with_outcome: Vec<_> = mods
.iter()
.filter(|r| r.outcome != Outcome::Pending)
.collect();
if with_outcome.is_empty() {
return None;
}
let successes = with_outcome
.iter()
.filter(|r| r.outcome == Outcome::Success)
.count();
Some(successes as f64 / with_outcome.len() as f64)
}
fn store_pattern(&mut self, pattern: CodePattern) {
self.memory.store_pattern(pattern.clone());
let serde_pat = CodePatternSerde::from(pattern);
if let Ok(json) = serde_json::to_string(&serde_pat) {
let _ = self.ops.hset(KEY_PATTERNS, &serde_pat.id, &json);
}
}
fn get_pattern(&self, id: &str) -> Option<CodePattern> {
self.memory.get_pattern(id).cloned()
}
fn update_baseline(&mut self, baseline: PerformanceBaseline) {
let metric = baseline.metric.clone();
self.memory.update_baseline(baseline.clone());
let serde_bl = PerformanceBaselineSerde::from(baseline);
if let Ok(json) = serde_json::to_string(&serde_bl) {
let _ = self.ops.hset(KEY_BASELINES, &metric, &json);
}
}
fn get_baseline(&self, metric: &str) -> Option<PerformanceBaseline> {
self.memory.get_baseline(metric).cloned()
}
fn record_dead_end(&mut self, dead_end: DeadEnd) {
let key = dead_end.key.clone();
self.memory.record_dead_end(dead_end.clone());
let _ = self.ops.sadd(KEY_DEADEND_SET, &key);
let serde_de = DeadEndSerde::from(dead_end);
if let Ok(json) = serde_json::to_string(&serde_de) {
let _ = self.ops.hset(KEY_DEADEND_DATA, &key, &json);
}
}
fn is_dead_end(&self, key: &str) -> bool {
self.memory.is_dead_end(key)
}
fn estimated_latency(&self) -> Duration {
Duration::from_millis(1)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::collections::{HashMap, HashSet};
pub(crate) struct MockRedisOps {
lists: HashMap<String, Vec<String>>,
hashes: HashMap<String, HashMap<String, String>>,
sets: HashMap<String, HashSet<String>>,
fail_ping: bool,
}
impl MockRedisOps {
pub(crate) fn new() -> Self {
Self {
lists: HashMap::new(),
hashes: HashMap::new(),
sets: HashMap::new(),
fail_ping: false,
}
}
}
impl RedisOps for MockRedisOps {
fn lpush(&mut self, key: &str, value: &str) -> Result<(), String> {
self.lists
.entry(key.to_string())
.or_default()
.insert(0, value.to_string());
Ok(())
}
fn ltrim(&mut self, key: &str, start: isize, stop: isize) -> Result<(), String> {
if let Some(list) = self.lists.get_mut(key) {
let len = list.len() as isize;
let s = start.max(0) as usize;
let e = if stop < 0 {
(len + stop).max(-1) as usize
} else {
stop.min(len - 1).max(-1) as usize
};
if s > e || s >= len as usize {
list.clear();
} else {
*list = list[s..=e].to_vec();
}
}
Ok(())
}
fn lrange(&mut self, key: &str, start: isize, stop: isize) -> Result<Vec<String>, String> {
let list = match self.lists.get(key) {
Some(l) => l,
None => return Ok(vec![]),
};
let len = list.len() as isize;
let s = start.max(0) as usize;
let e = if stop < 0 {
(len + stop).max(0) as usize
} else {
stop.min(len - 1).max(0) as usize
};
if s > e || s >= list.len() {
Ok(vec![])
} else {
Ok(list[s..=e].to_vec())
}
}
fn hset(&mut self, key: &str, field: &str, value: &str) -> Result<(), String> {
self.hashes
.entry(key.to_string())
.or_default()
.insert(field.to_string(), value.to_string());
Ok(())
}
fn hget(&mut self, key: &str, field: &str) -> Result<Option<String>, String> {
Ok(self.hashes.get(key).and_then(|h| h.get(field)).cloned())
}
fn sadd(&mut self, key: &str, member: &str) -> Result<(), String> {
self.sets
.entry(key.to_string())
.or_default()
.insert(member.to_string());
Ok(())
}
fn sismember(&mut self, key: &str, member: &str) -> Result<bool, String> {
Ok(self
.sets
.get(key)
.map(|s| s.contains(member))
.unwrap_or(false))
}
fn ping(&mut self) -> Result<(), String> {
if self.fail_ping {
Err("mock ping failure".to_string())
} else {
Ok(())
}
}
}
fn config() -> MemoryConfig {
MemoryConfig {
max_modifications: 10,
max_patterns: 10,
max_dead_ends: 10,
max_baselines: 10,
}
}
fn backend() -> RedisMemoryBackend {
RedisMemoryBackend::with_ops(Box::new(MockRedisOps::new()), config())
}
fn make_record(id: &str, outcome: Outcome) -> ModificationRecord {
ModificationRecord {
id: id.to_string(),
description: format!("desc {}", id),
affected_files: vec!["src/foo.rs".to_string()],
outcome,
metric_deltas: HashMap::new(),
notes: "".to_string(),
timestamp_ms: 1000,
}
}
fn make_pattern(id: &str) -> CodePattern {
CodePattern {
id: id.to_string(),
title: format!("Pattern {}", id),
content: "// content".to_string(),
tags: HashSet::from(["async".to_string()]),
use_count: 0,
created_at_ms: 0,
}
}
fn make_baseline(metric: &str) -> PerformanceBaseline {
PerformanceBaseline {
metric: metric.to_string(),
p50: 10.0,
p95: 20.0,
p99: 30.0,
mean: 15.0,
std_dev: 2.0,
sample_count: 100,
updated_at_ms: 0,
}
}
fn make_dead_end(key: &str) -> DeadEnd {
DeadEnd {
key: key.to_string(),
description: "desc".to_string(),
reason: "failed".to_string(),
related_signals: vec!["sig:a".to_string()],
recorded_at_ms: 0,
}
}
#[test]
fn with_ops_creates_backend() {
let b = backend();
assert!(b.success_rate().is_none());
}
#[test]
fn record_modification_adds_to_memory() {
let mut b = backend();
b.record_modification(make_record("r1", Outcome::Success));
assert!(b.get_modification("r1").is_some());
}
#[test]
fn record_modification_returns_id() {
let mut b = backend();
let id = b.record_modification(make_record("r2", Outcome::Failure));
assert_eq!(id, "r2");
}
#[test]
fn record_modification_pushes_to_redis() {
let mock = MockRedisOps::new();
let mut b = RedisMemoryBackend::with_ops(Box::new(mock), config());
b.record_modification(make_record("r3", Outcome::Success));
b.record_modification(make_record("r4", Outcome::Success));
assert_eq!(b.recent_modifications(10).len(), 2);
}
#[test]
fn get_modification_returns_none_for_unknown() {
let b = backend();
assert!(b.get_modification("nope").is_none());
}
#[test]
fn get_modification_returns_stored() {
let mut b = backend();
b.record_modification(make_record("x", Outcome::Success));
let r = b.get_modification("x").unwrap();
assert_eq!(r.id, "x");
}
#[test]
fn recent_modifications_empty_when_empty() {
let b = backend();
assert!(b.recent_modifications(5).is_empty());
}
#[test]
fn recent_modifications_returns_n_most_recent() {
let mut b = backend();
for i in 0..5u32 {
b.record_modification(make_record(&i.to_string(), Outcome::Success));
}
let recent = b.recent_modifications(3);
assert_eq!(recent.len(), 3);
assert_eq!(recent[0].id, "4");
}
#[test]
fn recent_modifications_caps_at_available() {
let mut b = backend();
b.record_modification(make_record("only", Outcome::Success));
let recent = b.recent_modifications(100);
assert_eq!(recent.len(), 1);
}
#[test]
fn success_rate_none_when_no_outcomes() {
let b = backend();
assert!(b.success_rate().is_none());
}
#[test]
fn success_rate_none_when_all_pending() {
let mut b = backend();
b.record_modification(make_record("p", Outcome::Pending));
assert!(b.success_rate().is_none());
}
#[test]
fn success_rate_one_when_all_success() {
let mut b = backend();
b.record_modification(make_record("a", Outcome::Success));
b.record_modification(make_record("b", Outcome::Success));
let rate = b.success_rate().unwrap();
assert!((rate - 1.0).abs() < 1e-9);
}
#[test]
fn success_rate_zero_when_all_failure() {
let mut b = backend();
b.record_modification(make_record("a", Outcome::Failure));
let rate = b.success_rate().unwrap();
assert!((rate - 0.0).abs() < 1e-9);
}
#[test]
fn success_rate_excludes_pending() {
let mut b = backend();
b.record_modification(make_record("s", Outcome::Success));
b.record_modification(make_record("p", Outcome::Pending));
let rate = b.success_rate().unwrap();
assert!((rate - 1.0).abs() < 1e-9);
}
#[test]
fn store_pattern_adds_to_redis() {
let mut b = backend();
b.store_pattern(make_pattern("p1"));
assert!(b.get_pattern("p1").is_some());
}
#[test]
fn get_pattern_returns_none_for_unknown() {
let b = backend();
assert!(b.get_pattern("ghost").is_none());
}
#[test]
fn get_pattern_returns_stored() {
let mut b = backend();
b.store_pattern(make_pattern("p2"));
let p = b.get_pattern("p2").unwrap();
assert_eq!(p.id, "p2");
}
#[test]
fn update_baseline_stores_in_redis() {
let mut b = backend();
b.update_baseline(make_baseline("lat"));
assert!(b.get_baseline("lat").is_some());
}
#[test]
fn get_baseline_returns_none_for_unknown() {
let b = backend();
assert!(b.get_baseline("missing").is_none());
}
#[test]
fn get_baseline_returns_stored() {
let mut b = backend();
b.update_baseline(make_baseline("thr"));
let bl = b.get_baseline("thr").unwrap();
assert_eq!(bl.metric, "thr");
}
#[test]
fn record_dead_end_marks_as_dead() {
let mut b = backend();
b.record_dead_end(make_dead_end("approach-a"));
assert!(b.is_dead_end("approach-a"));
}
#[test]
fn is_dead_end_false_when_not_recorded() {
let b = backend();
assert!(!b.is_dead_end("approach-x"));
}
#[test]
fn is_dead_end_true_after_record() {
let mut b = backend();
b.record_dead_end(make_dead_end("bad-idea"));
assert!(b.is_dead_end("bad-idea"));
}
#[test]
fn estimated_latency_is_one_ms() {
let b = backend();
assert_eq!(b.estimated_latency(), Duration::from_millis(1));
}
#[test]
fn mock_redis_lpush_lrange_roundtrip() {
let mut mock = MockRedisOps::new();
mock.lpush("k", "v1").unwrap();
mock.lpush("k", "v2").unwrap();
let vals = mock.lrange("k", 0, -1).unwrap();
assert_eq!(vals, vec!["v2", "v1"]);
}
#[test]
fn mock_redis_hset_hget_roundtrip() {
let mut mock = MockRedisOps::new();
mock.hset("h", "f", "val").unwrap();
let v = mock.hget("h", "f").unwrap();
assert_eq!(v, Some("val".to_string()));
}
#[test]
fn mock_redis_sadd_sismember() {
let mut mock = MockRedisOps::new();
mock.sadd("s", "m1").unwrap();
assert!(mock.sismember("s", "m1").unwrap());
assert!(!mock.sismember("s", "m2").unwrap());
}
#[test]
fn mock_redis_ltrim_caps_list() {
let mut mock = MockRedisOps::new();
for i in 0..5 {
mock.lpush("k", &i.to_string()).unwrap();
}
mock.ltrim("k", 0, 2).unwrap();
let vals = mock.lrange("k", 0, -1).unwrap();
assert_eq!(vals.len(), 3);
}
#[test]
fn record_modification_respects_max_cap() {
let mut b = RedisMemoryBackend::with_ops(
Box::new(MockRedisOps::new()),
MemoryConfig {
max_modifications: 3,
max_patterns: 10,
max_dead_ends: 10,
max_baselines: 10,
},
);
for i in 0..5u32 {
b.record_modification(make_record(&i.to_string(), Outcome::Success));
}
let recent = b.recent_modifications(10);
assert_eq!(recent.len(), 3);
}
#[test]
fn restore_loads_modifications_from_redis() {
let mut mock = MockRedisOps::new();
let rec = ModificationRecordSerde {
id: "restored".to_string(),
description: "test".to_string(),
affected_files: vec![],
outcome: "success".to_string(),
metric_deltas: HashMap::new(),
notes: "".to_string(),
timestamp_ms: 999,
};
let json = serde_json::to_string(&rec).unwrap();
mock.lpush(KEY_MODS, &json).unwrap();
let mut b = RedisMemoryBackend::with_ops(Box::new(mock), config());
b.restore().unwrap();
assert!(b.get_modification("restored").is_some());
}
#[test]
fn restore_returns_count() {
let mut mock = MockRedisOps::new();
for i in 0..3u32 {
let rec = ModificationRecordSerde {
id: i.to_string(),
description: "".to_string(),
affected_files: vec![],
outcome: "success".to_string(),
metric_deltas: HashMap::new(),
notes: "".to_string(),
timestamp_ms: 0,
};
let json = serde_json::to_string(&rec).unwrap();
mock.lpush(KEY_MODS, &json).unwrap();
}
let mut b = RedisMemoryBackend::with_ops(Box::new(mock), config());
let count = b.restore().unwrap();
assert_eq!(count, 3);
}
#[test]
fn modification_serializes_and_deserializes() {
let rec = make_record("serde-test", Outcome::RolledBack);
let serde_rec = ModificationRecordSerde::from(rec.clone());
let json = serde_json::to_string(&serde_rec).unwrap();
let back: ModificationRecordSerde = serde_json::from_str(&json).unwrap();
let restored = ModificationRecord::try_from(back).unwrap();
assert_eq!(restored.id, rec.id);
assert_eq!(restored.outcome, rec.outcome);
}
#[test]
fn pattern_serializes_and_deserializes() {
let p = make_pattern("pat-serde");
let serde_p = CodePatternSerde::from(p.clone());
let json = serde_json::to_string(&serde_p).unwrap();
let back: CodePatternSerde = serde_json::from_str(&json).unwrap();
let restored = CodePattern::from(back);
assert_eq!(restored.id, p.id);
}
#[test]
fn baseline_serializes_and_deserializes() {
let bl = make_baseline("lat-serde");
let serde_bl = PerformanceBaselineSerde::from(bl.clone());
let json = serde_json::to_string(&serde_bl).unwrap();
let back: PerformanceBaselineSerde = serde_json::from_str(&json).unwrap();
let restored = PerformanceBaseline::from(back);
assert_eq!(restored.metric, bl.metric);
assert!((restored.p95 - bl.p95).abs() < 1e-9);
}
#[test]
fn dead_end_serializes_and_deserializes() {
let de = make_dead_end("de-serde");
let serde_de = DeadEndSerde::from(de.clone());
let json = serde_json::to_string(&serde_de).unwrap();
let back: DeadEndSerde = serde_json::from_str(&json).unwrap();
let restored = DeadEnd::from(back);
assert_eq!(restored.key, de.key);
assert_eq!(restored.reason, de.reason);
}
#[test]
fn two_modifications_both_stored() {
let mut b = backend();
b.record_modification(make_record("m1", Outcome::Success));
b.record_modification(make_record("m2", Outcome::Failure));
assert!(b.get_modification("m1").is_some());
assert!(b.get_modification("m2").is_some());
}
}