feat(intelligence): complete Phase 2-3 migration to Rust
Phase 2 - Core Engines: - Heartbeat Engine: Periodic proactive checks with quiet hours support - Context Compactor: Token estimation and message summarization - CJK character handling (1.5 tokens per char) - Rule-based summary generation Phase 3 - Advanced Features: - Reflection Engine: Pattern analysis and improvement suggestions - Agent Identity: SOUL.md/AGENTS.md/USER.md management - Proposal-based changes (requires user approval) - Snapshot history for rollback All modules include: - Tauri commands for frontend integration - Unit tests - Re-exported types via mod.rs Reference: docs/plans/INTELLIGENCE-LAYER-MIGRATION.md Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
651
desktop/src-tauri/src/intelligence/identity.rs
Normal file
651
desktop/src-tauri/src/intelligence/identity.rs
Normal file
@@ -0,0 +1,651 @@
|
||||
//! Agent Identity Manager - Per-agent dynamic identity files
|
||||
//!
|
||||
//! Manages SOUL.md, AGENTS.md, USER.md per agent with:
|
||||
//! - Per-agent isolated identity directories
|
||||
//! - USER.md auto-update by agent (stores learned preferences)
|
||||
//! - SOUL.md/AGENTS.md change proposals (require user approval)
|
||||
//! - Snapshot history for rollback
|
||||
//!
|
||||
//! Phase 3 of Intelligence Layer Migration.
|
||||
//! Reference: ZCLAW_AGENT_INTELLIGENCE_EVOLUTION.md §6.2.3
|
||||
|
||||
use chrono::{DateTime, Utc};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
// === Types ===
|
||||
|
||||
/// Identity files for an agent
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct IdentityFiles {
|
||||
pub soul: String,
|
||||
pub instructions: String,
|
||||
pub user_profile: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub heartbeat: Option<String>,
|
||||
}
|
||||
|
||||
/// Proposal for identity change (requires user approval)
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct IdentityChangeProposal {
|
||||
pub id: String,
|
||||
pub agent_id: String,
|
||||
pub file: IdentityFile,
|
||||
pub reason: String,
|
||||
pub current_content: String,
|
||||
pub suggested_content: String,
|
||||
pub status: ProposalStatus,
|
||||
pub created_at: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum IdentityFile {
|
||||
Soul,
|
||||
Instructions,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum ProposalStatus {
|
||||
Pending,
|
||||
Approved,
|
||||
Rejected,
|
||||
}
|
||||
|
||||
/// Snapshot for rollback
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct IdentitySnapshot {
|
||||
pub id: String,
|
||||
pub agent_id: String,
|
||||
pub files: IdentityFiles,
|
||||
pub timestamp: String,
|
||||
pub reason: String,
|
||||
}
|
||||
|
||||
// === Default Identity Content ===
|
||||
|
||||
fn default_soul() -> String {
|
||||
r#"# ZCLAW 人格
|
||||
|
||||
你是 ZCLAW(小龙虾),一个基于 OpenClaw 定制的中文 AI 助手。
|
||||
|
||||
## 核心特质
|
||||
|
||||
- **高效执行**: 你不只是出主意,你会真正动手完成任务
|
||||
- **中文优先**: 默认使用中文交流,必要时切换英文
|
||||
- **专业可靠**: 对技术问题给出精确答案,不确定时坦诚说明
|
||||
- **持续成长**: 你会记住与用户的交互,不断改进自己的服务方式
|
||||
|
||||
## 语气
|
||||
|
||||
简洁、专业、友好。避免过度客套,直接给出有用信息。"#.to_string()
|
||||
}
|
||||
|
||||
fn default_instructions() -> String {
|
||||
r#"# Agent 指令
|
||||
|
||||
## 操作规范
|
||||
|
||||
1. 执行文件操作前,先确认目标路径
|
||||
2. 执行 Shell 命令前,评估安全风险
|
||||
3. 长时间任务需定期汇报进度
|
||||
4. 优先使用中文回复
|
||||
|
||||
## 记忆管理
|
||||
|
||||
- 重要的用户偏好自动记录
|
||||
- 项目上下文保存到工作区
|
||||
- 对话结束时总结关键信息"#.to_string()
|
||||
}
|
||||
|
||||
fn default_user_profile() -> String {
|
||||
r#"# 用户画像
|
||||
|
||||
_尚未收集到用户偏好信息。随着交互积累,此文件将自动更新。_"#.to_string()
|
||||
}
|
||||
|
||||
// === Agent Identity Manager ===
|
||||
|
||||
pub struct AgentIdentityManager {
|
||||
identities: HashMap<String, IdentityFiles>,
|
||||
proposals: Vec<IdentityChangeProposal>,
|
||||
snapshots: Vec<IdentitySnapshot>,
|
||||
snapshot_counter: usize,
|
||||
}
|
||||
|
||||
impl AgentIdentityManager {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
identities: HashMap::new(),
|
||||
proposals: Vec::new(),
|
||||
snapshots: Vec::new(),
|
||||
snapshot_counter: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get identity files for an agent (creates default if not exists)
|
||||
pub fn get_identity(&mut self, agent_id: &str) -> IdentityFiles {
|
||||
if let Some(existing) = self.identities.get(agent_id) {
|
||||
return existing.clone();
|
||||
}
|
||||
|
||||
// Initialize with defaults
|
||||
let defaults = IdentityFiles {
|
||||
soul: default_soul(),
|
||||
instructions: default_instructions(),
|
||||
user_profile: default_user_profile(),
|
||||
heartbeat: None,
|
||||
};
|
||||
self.identities.insert(agent_id.to_string(), defaults.clone());
|
||||
defaults
|
||||
}
|
||||
|
||||
/// Get a specific file content
|
||||
pub fn get_file(&mut self, agent_id: &str, file: IdentityFile) -> String {
|
||||
let identity = self.get_identity(agent_id);
|
||||
match file {
|
||||
IdentityFile::Soul => identity.soul,
|
||||
IdentityFile::Instructions => identity.instructions,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build system prompt from identity files
|
||||
pub fn build_system_prompt(&mut self, agent_id: &str, memory_context: Option<&str>) -> String {
|
||||
let identity = self.get_identity(agent_id);
|
||||
let mut sections = Vec::new();
|
||||
|
||||
if !identity.soul.is_empty() {
|
||||
sections.push(identity.soul.clone());
|
||||
}
|
||||
if !identity.instructions.is_empty() {
|
||||
sections.push(identity.instructions.clone());
|
||||
}
|
||||
if !identity.user_profile.is_empty()
|
||||
&& identity.user_profile != default_user_profile()
|
||||
{
|
||||
sections.push(format!("## 用户画像\n{}", identity.user_profile));
|
||||
}
|
||||
if let Some(ctx) = memory_context {
|
||||
sections.push(ctx.to_string());
|
||||
}
|
||||
|
||||
sections.join("\n\n")
|
||||
}
|
||||
|
||||
/// Update user profile (auto, no approval needed)
|
||||
pub fn update_user_profile(&mut self, agent_id: &str, new_content: &str) {
|
||||
let identity = self.get_identity(agent_id);
|
||||
let _old_content = identity.user_profile.clone();
|
||||
|
||||
// Create snapshot before update
|
||||
self.create_snapshot(agent_id, "Auto-update USER.md");
|
||||
|
||||
let mut updated = identity.clone();
|
||||
updated.user_profile = new_content.to_string();
|
||||
self.identities.insert(agent_id.to_string(), updated);
|
||||
}
|
||||
|
||||
/// Append to user profile
|
||||
pub fn append_to_user_profile(&mut self, agent_id: &str, addition: &str) {
|
||||
let identity = self.get_identity(agent_id);
|
||||
let updated = format!("{}\n\n{}", identity.user_profile.trim_end(), addition);
|
||||
self.update_user_profile(agent_id, &updated);
|
||||
}
|
||||
|
||||
/// Propose a change to soul or instructions (requires approval)
|
||||
pub fn propose_change(
|
||||
&mut self,
|
||||
agent_id: &str,
|
||||
file: IdentityFile,
|
||||
suggested_content: &str,
|
||||
reason: &str,
|
||||
) -> IdentityChangeProposal {
|
||||
let identity = self.get_identity(agent_id);
|
||||
let current_content = match file {
|
||||
IdentityFile::Soul => identity.soul.clone(),
|
||||
IdentityFile::Instructions => identity.instructions.clone(),
|
||||
};
|
||||
|
||||
let proposal = IdentityChangeProposal {
|
||||
id: format!("prop_{}_{}", Utc::now().timestamp(), rand_id()),
|
||||
agent_id: agent_id.to_string(),
|
||||
file,
|
||||
reason: reason.to_string(),
|
||||
current_content,
|
||||
suggested_content: suggested_content.to_string(),
|
||||
status: ProposalStatus::Pending,
|
||||
created_at: Utc::now().to_rfc3339(),
|
||||
};
|
||||
|
||||
self.proposals.push(proposal.clone());
|
||||
proposal
|
||||
}
|
||||
|
||||
/// Approve a pending proposal
|
||||
pub fn approve_proposal(&mut self, proposal_id: &str) -> Result<IdentityFiles, String> {
|
||||
let proposal_idx = self
|
||||
.proposals
|
||||
.iter()
|
||||
.position(|p| p.id == proposal_id && p.status == ProposalStatus::Pending)
|
||||
.ok_or_else(|| "Proposal not found or not pending".to_string())?;
|
||||
|
||||
let proposal = &self.proposals[proposal_idx];
|
||||
let agent_id = proposal.agent_id.clone();
|
||||
let file = proposal.file.clone();
|
||||
|
||||
// Create snapshot before applying
|
||||
self.create_snapshot(&agent_id, &format!("Approved proposal: {}", proposal.reason));
|
||||
|
||||
// Get current identity and update
|
||||
let identity = self.get_identity(&agent_id);
|
||||
let mut updated = identity.clone();
|
||||
|
||||
match file {
|
||||
IdentityFile::Soul => updated.soul = proposal.suggested_content.clone(),
|
||||
IdentityFile::Instructions => {
|
||||
updated.instructions = proposal.suggested_content.clone()
|
||||
}
|
||||
}
|
||||
|
||||
self.identities.insert(agent_id.clone(), updated.clone());
|
||||
|
||||
// Update proposal status
|
||||
self.proposals[proposal_idx].status = ProposalStatus::Approved;
|
||||
|
||||
Ok(updated)
|
||||
}
|
||||
|
||||
/// Reject a pending proposal
|
||||
pub fn reject_proposal(&mut self, proposal_id: &str) -> Result<(), String> {
|
||||
let proposal = self
|
||||
.proposals
|
||||
.iter_mut()
|
||||
.find(|p| p.id == proposal_id && p.status == ProposalStatus::Pending)
|
||||
.ok_or_else(|| "Proposal not found or not pending".to_string())?;
|
||||
|
||||
proposal.status = ProposalStatus::Rejected;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get pending proposals for an agent (or all agents if None)
|
||||
pub fn get_pending_proposals(&self, agent_id: Option<&str>) -> Vec<&IdentityChangeProposal> {
|
||||
self.proposals
|
||||
.iter()
|
||||
.filter(|p| {
|
||||
p.status == ProposalStatus::Pending
|
||||
&& agent_id.map_or(true, |id| p.agent_id == id)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Direct file update (user explicitly edits in UI)
|
||||
pub fn update_file(
|
||||
&mut self,
|
||||
agent_id: &str,
|
||||
file: &str,
|
||||
content: &str,
|
||||
) -> Result<(), String> {
|
||||
let identity = self.get_identity(agent_id);
|
||||
self.create_snapshot(agent_id, &format!("Manual edit: {}", file));
|
||||
|
||||
let mut updated = identity.clone();
|
||||
match file {
|
||||
"soul" => updated.soul = content.to_string(),
|
||||
"instructions" => updated.instructions = content.to_string(),
|
||||
"userProfile" | "user_profile" => updated.user_profile = content.to_string(),
|
||||
_ => return Err(format!("Unknown file: {}", file)),
|
||||
}
|
||||
|
||||
self.identities.insert(agent_id.to_string(), updated);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create a snapshot
|
||||
fn create_snapshot(&mut self, agent_id: &str, reason: &str) {
|
||||
let identity = self.get_identity(agent_id);
|
||||
self.snapshot_counter += 1;
|
||||
|
||||
self.snapshots.push(IdentitySnapshot {
|
||||
id: format!(
|
||||
"snap_{}_{}_{}",
|
||||
Utc::now().timestamp(),
|
||||
self.snapshot_counter,
|
||||
rand_id()
|
||||
),
|
||||
agent_id: agent_id.to_string(),
|
||||
files: identity,
|
||||
timestamp: Utc::now().to_rfc3339(),
|
||||
reason: reason.to_string(),
|
||||
});
|
||||
|
||||
// Keep only last 50 snapshots per agent
|
||||
let agent_snapshots: Vec<_> = self
|
||||
.snapshots
|
||||
.iter()
|
||||
.filter(|s| s.agent_id == agent_id)
|
||||
.collect();
|
||||
if agent_snapshots.len() > 50 {
|
||||
// Remove oldest snapshots for this agent
|
||||
self.snapshots.retain(|s| {
|
||||
s.agent_id != agent_id
|
||||
|| agent_snapshots
|
||||
.iter()
|
||||
.rev()
|
||||
.take(50)
|
||||
.any(|&s_ref| s_ref.id == s.id)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Get snapshots for an agent
|
||||
pub fn get_snapshots(&self, agent_id: &str, limit: usize) -> Vec<&IdentitySnapshot> {
|
||||
let mut filtered: Vec<_> = self
|
||||
.snapshots
|
||||
.iter()
|
||||
.filter(|s| s.agent_id == agent_id)
|
||||
.collect();
|
||||
filtered.sort_by(|a, b| b.timestamp.cmp(&a.timestamp));
|
||||
filtered.into_iter().take(limit).collect()
|
||||
}
|
||||
|
||||
/// Restore a snapshot
|
||||
pub fn restore_snapshot(&mut self, agent_id: &str, snapshot_id: &str) -> Result<(), String> {
|
||||
let snapshot = self
|
||||
.snapshots
|
||||
.iter()
|
||||
.find(|s| s.agent_id == agent_id && s.id == snapshot_id)
|
||||
.ok_or_else(|| "Snapshot not found".to_string())?;
|
||||
|
||||
// Create snapshot before rollback
|
||||
self.create_snapshot(
|
||||
agent_id,
|
||||
&format!("Rollback to {}", snapshot.timestamp),
|
||||
);
|
||||
|
||||
self.identities
|
||||
.insert(agent_id.to_string(), snapshot.files.clone());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List all agents with identities
|
||||
pub fn list_agents(&self) -> Vec<String> {
|
||||
self.identities.keys().cloned().collect()
|
||||
}
|
||||
|
||||
/// Delete an agent's identity
|
||||
pub fn delete_agent(&mut self, agent_id: &str) {
|
||||
self.identities.remove(agent_id);
|
||||
self.proposals.retain(|p| p.agent_id != agent_id);
|
||||
self.snapshots.retain(|s| s.agent_id != agent_id);
|
||||
}
|
||||
|
||||
/// Export all identities for backup
|
||||
pub fn export_all(&self) -> HashMap<String, IdentityFiles> {
|
||||
self.identities.clone()
|
||||
}
|
||||
|
||||
/// Import identities from backup
|
||||
pub fn import(&mut self, identities: HashMap<String, IdentityFiles>) {
|
||||
for (agent_id, files) in identities {
|
||||
self.identities.insert(agent_id, files);
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all proposals (for debugging)
|
||||
pub fn get_all_proposals(&self) -> &[IdentityChangeProposal] {
|
||||
&self.proposals
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for AgentIdentityManager {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate random ID suffix
|
||||
fn rand_id() -> String {
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
static COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||
let count = COUNTER.fetch_add(1, Ordering::Relaxed);
|
||||
format!("{:04x}", count % 0x10000)
|
||||
}
|
||||
|
||||
// === Tauri Commands ===
|
||||
|
||||
use std::sync::Arc;
|
||||
use tokio::sync::Mutex;
|
||||
|
||||
pub type IdentityManagerState = Arc<Mutex<AgentIdentityManager>>;
|
||||
|
||||
/// Initialize identity manager
|
||||
#[tauri::command]
|
||||
pub async fn identity_init() -> Result<IdentityManagerState, String> {
|
||||
Ok(Arc::new(Mutex::new(AgentIdentityManager::new())))
|
||||
}
|
||||
|
||||
/// Get identity files for an agent
|
||||
#[tauri::command]
|
||||
pub async fn identity_get(
|
||||
agent_id: String,
|
||||
state: tauri::State<'_, IdentityManagerState>,
|
||||
) -> Result<IdentityFiles, String> {
|
||||
let mut manager = state.lock().await;
|
||||
Ok(manager.get_identity(&agent_id))
|
||||
}
|
||||
|
||||
/// Get a specific file
|
||||
#[tauri::command]
|
||||
pub async fn identity_get_file(
|
||||
agent_id: String,
|
||||
file: String,
|
||||
state: tauri::State<'_, IdentityManagerState>,
|
||||
) -> Result<String, String> {
|
||||
let mut manager = state.lock().await;
|
||||
let file_type = match file.as_str() {
|
||||
"soul" => IdentityFile::Soul,
|
||||
"instructions" => IdentityFile::Instructions,
|
||||
_ => return Err(format!("Unknown file: {}", file)),
|
||||
};
|
||||
Ok(manager.get_file(&agent_id, file_type))
|
||||
}
|
||||
|
||||
/// Build system prompt
|
||||
#[tauri::command]
|
||||
pub async fn identity_build_prompt(
|
||||
agent_id: String,
|
||||
memory_context: Option<String>,
|
||||
state: tauri::State<'_, IdentityManagerState>,
|
||||
) -> Result<String, String> {
|
||||
let mut manager = state.lock().await;
|
||||
Ok(manager.build_system_prompt(&agent_id, memory_context.as_deref()))
|
||||
}
|
||||
|
||||
/// Update user profile (auto)
|
||||
#[tauri::command]
|
||||
pub async fn identity_update_user_profile(
|
||||
agent_id: String,
|
||||
content: String,
|
||||
state: tauri::State<'_, IdentityManagerState>,
|
||||
) -> Result<(), String> {
|
||||
let mut manager = state.lock().await;
|
||||
manager.update_user_profile(&agent_id, &content);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Append to user profile
|
||||
#[tauri::command]
|
||||
pub async fn identity_append_user_profile(
|
||||
agent_id: String,
|
||||
addition: String,
|
||||
state: tauri::State<'_, IdentityManagerState>,
|
||||
) -> Result<(), String> {
|
||||
let mut manager = state.lock().await;
|
||||
manager.append_to_user_profile(&agent_id, &addition);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Propose a change
|
||||
#[tauri::command]
|
||||
pub async fn identity_propose_change(
|
||||
agent_id: String,
|
||||
file: String,
|
||||
suggested_content: String,
|
||||
reason: String,
|
||||
state: tauri::State<'_, IdentityManagerState>,
|
||||
) -> Result<IdentityChangeProposal, String> {
|
||||
let mut manager = state.lock().await;
|
||||
let file_type = match file.as_str() {
|
||||
"soul" => IdentityFile::Soul,
|
||||
"instructions" => IdentityFile::Instructions,
|
||||
_ => return Err(format!("Unknown file: {}", file)),
|
||||
};
|
||||
Ok(manager.propose_change(&agent_id, file_type, &suggested_content, &reason))
|
||||
}
|
||||
|
||||
/// Approve a proposal
|
||||
#[tauri::command]
|
||||
pub async fn identity_approve_proposal(
|
||||
proposal_id: String,
|
||||
state: tauri::State<'_, IdentityManagerState>,
|
||||
) -> Result<IdentityFiles, String> {
|
||||
let mut manager = state.lock().await;
|
||||
manager.approve_proposal(&proposal_id)
|
||||
}
|
||||
|
||||
/// Reject a proposal
|
||||
#[tauri::command]
|
||||
pub async fn identity_reject_proposal(
|
||||
proposal_id: String,
|
||||
state: tauri::State<'_, IdentityManagerState>,
|
||||
) -> Result<(), String> {
|
||||
let mut manager = state.lock().await;
|
||||
manager.reject_proposal(&proposal_id)
|
||||
}
|
||||
|
||||
/// Get pending proposals
|
||||
#[tauri::command]
|
||||
pub async fn identity_get_pending_proposals(
|
||||
agent_id: Option<String>,
|
||||
state: tauri::State<'_, IdentityManagerState>,
|
||||
) -> Result<Vec<IdentityChangeProposal>, String> {
|
||||
let manager = state.lock().await;
|
||||
Ok(manager
|
||||
.get_pending_proposals(agent_id.as_deref())
|
||||
.into_iter()
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Update file directly
|
||||
#[tauri::command]
|
||||
pub async fn identity_update_file(
|
||||
agent_id: String,
|
||||
file: String,
|
||||
content: String,
|
||||
state: tauri::State<'_, IdentityManagerState>,
|
||||
) -> Result<(), String> {
|
||||
let mut manager = state.lock().await;
|
||||
manager.update_file(&agent_id, &file, &content)
|
||||
}
|
||||
|
||||
/// Get snapshots
|
||||
#[tauri::command]
|
||||
pub async fn identity_get_snapshots(
|
||||
agent_id: String,
|
||||
limit: Option<usize>,
|
||||
state: tauri::State<'_, IdentityManagerState>,
|
||||
) -> Result<Vec<IdentitySnapshot>, String> {
|
||||
let manager = state.lock().await;
|
||||
Ok(manager
|
||||
.get_snapshots(&agent_id, limit.unwrap_or(10))
|
||||
.into_iter()
|
||||
.cloned()
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Restore snapshot
|
||||
#[tauri::command]
|
||||
pub async fn identity_restore_snapshot(
|
||||
agent_id: String,
|
||||
snapshot_id: String,
|
||||
state: tauri::State<'_, IdentityManagerState>,
|
||||
) -> Result<(), String> {
|
||||
let mut manager = state.lock().await;
|
||||
manager.restore_snapshot(&agent_id, &snapshot_id)
|
||||
}
|
||||
|
||||
/// List agents
|
||||
#[tauri::command]
|
||||
pub async fn identity_list_agents(
|
||||
state: tauri::State<'_, IdentityManagerState>,
|
||||
) -> Result<Vec<String>, String> {
|
||||
let manager = state.lock().await;
|
||||
Ok(manager.list_agents())
|
||||
}
|
||||
|
||||
/// Delete agent identity
|
||||
#[tauri::command]
|
||||
pub async fn identity_delete_agent(
|
||||
agent_id: String,
|
||||
state: tauri::State<'_, IdentityManagerState>,
|
||||
) -> Result<(), String> {
|
||||
let mut manager = state.lock().await;
|
||||
manager.delete_agent(&agent_id);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_get_identity_creates_default() {
|
||||
let mut manager = AgentIdentityManager::new();
|
||||
let identity = manager.get_identity("test-agent");
|
||||
assert!(!identity.soul.is_empty());
|
||||
assert!(!identity.instructions.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_update_user_profile() {
|
||||
let mut manager = AgentIdentityManager::new();
|
||||
manager.update_user_profile("test-agent", "New profile content");
|
||||
let identity = manager.get_identity("test-agent");
|
||||
assert_eq!(identity.user_profile, "New profile content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_proposal_flow() {
|
||||
let mut manager = AgentIdentityManager::new();
|
||||
let proposal = manager.propose_change(
|
||||
"test-agent",
|
||||
IdentityFile::Soul,
|
||||
"New soul content",
|
||||
"Test proposal",
|
||||
);
|
||||
|
||||
assert_eq!(proposal.status, ProposalStatus::Pending);
|
||||
|
||||
let pending = manager.get_pending_proposals(None);
|
||||
assert_eq!(pending.len(), 1);
|
||||
|
||||
// Approve
|
||||
let result = manager.approve_proposal(&proposal.id);
|
||||
assert!(result.is_ok());
|
||||
|
||||
let identity = manager.get_identity("test-agent");
|
||||
assert_eq!(identity.soul, "New soul content");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_snapshots() {
|
||||
let mut manager = AgentIdentityManager::new();
|
||||
manager.update_user_profile("test-agent", "First update");
|
||||
manager.update_user_profile("test-agent", "Second update");
|
||||
|
||||
let snapshots = manager.get_snapshots("test-agent", 10);
|
||||
assert!(snapshots.len() >= 2);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user