diff --git a/crates/gyandoot-agent/src/agent_loop.rs b/crates/gyandoot-agent/src/agent_loop.rs index a7af987..12ebd00 100644 --- a/crates/gyandoot-agent/src/agent_loop.rs +++ b/crates/gyandoot-agent/src/agent_loop.rs @@ -159,8 +159,59 @@ impl AgentLoop { let _guard = span.enter(); info!("Agent loop started (interactive)"); + // ── Identity & memory injection on startup ───────────────────── + let (agent_name, agent_role, user_name, user_role) = { + let memory = self.memory.lock().await; + let (l0, _) = memory.identity_get().unwrap_or_default(); + if !l0.is_empty() && !self.config.identity.agent_name.is_empty() { + ( + self.config.identity.agent_name.clone(), + self.config.identity.agent_role.clone(), + self.config.identity.user_name.clone(), + self.config.identity.user_role.clone(), + ) + } else if !l0.is_empty() { + // Parse from L0: "name: X\nrole: Y\nuser: Z (W)\n" + let mut parts = l0.lines().filter_map(|l| l.split_once(": ").map(|(_, v)| v.to_string())).collect::>(); + ( + parts.get(0).cloned().unwrap_or_default(), + parts.get(1).cloned().unwrap_or_default(), + parts.get(2).and_then(|u| u.split_once(" (").map(|(n, _)| n.to_string())).unwrap_or_default(), + parts.get(2).and_then(|u| u.split_once(" (").and_then(|(_, r)| r.trim_end_matches(')').to_string().into())).unwrap_or_default(), + ) + } else { + (String::new(), String::new(), String::new(), String::new()) + } + }; + + // Build system prompt with identity + dialogue memory + let mut system_prompt = self.config.system_prompt.clone(); + if !agent_name.is_empty() { + system_prompt = format!( + "{}\n\n## Your Identity\n- Name: {}\n- Role: {}\n- You are speaking to: {} ({})\n\n## Memory\nYou have a verbatim dialogue log with your user. Use everything from past conversations as context when answering.", + system_prompt, + agent_name, + if agent_role.is_empty() { "AI Assistant" } else { &agent_role }, + if user_name.is_empty() { "the User" } else { &user_name }, + if user_role.is_empty() { "User" } else { &user_role }, + ); + } + + // Inject recent dialogue from verbatim memory + { + let memory = self.memory.lock().await; + if let Ok(dialogue) = memory.dialogue_read(8000).await { + if !dialogue.is_empty() { + system_prompt = format!( + "{}\n\n## Past Conversations (Verbatim Memory)\n{}\n---\nContinue the conversation naturally.", + system_prompt, dialogue + ); + } + } + } + let mut messages: Vec = - vec![Message::new(MessageRole::System, &self.config.system_prompt)]; + vec![Message::new(MessageRole::System, &system_prompt)]; loop { // 1. Read user input @@ -174,19 +225,26 @@ impl AgentLoop { // 3. Sanitize let clean_input = self.sanitizer.sanitize_input(&user_input, &gyandoot_security::sanitizer::SanitizerConfig::default()); - messages.push(Message::new(MessageRole::User, clean_input)); + messages.push(Message::new(MessageRole::User, clean_input.clone())); // 4. Run the core turn (LLM + tool execution) let new_messages = self.execute_turn(messages).await?; messages = new_messages; - // 5. Store conversation turn in memory + // 5. Store conversation turn in memory (transcript + verbatim dialogue) { let memory = self.memory.lock().await; + // Daily transcript memory .transcript_append("user", &user_input) .await .unwrap_or_else(|e| warn!("Failed to store user message: {e}")); + // Verbatim dialogue — word by word, line by line + memory + .dialogue_append("user", &user_input) + .await + .unwrap_or_else(|e| warn!("Failed to store user dialogue: {e}")); + // Store last assistant response if let Some(last) = messages.last() { if last.role == MessageRole::Assistant { @@ -194,6 +252,10 @@ impl AgentLoop { .transcript_append("assistant", &last.content) .await .unwrap_or_else(|e| warn!("Failed to store assistant message: {e}")); + memory + .dialogue_append("assistant", &last.content) + .await + .unwrap_or_else(|e| warn!("Failed to store assistant dialogue: {e}")); } } } diff --git a/crates/gyandoot-core/src/config.rs b/crates/gyandoot-core/src/config.rs index 7712189..367599f 100644 --- a/crates/gyandoot-core/src/config.rs +++ b/crates/gyandoot-core/src/config.rs @@ -29,6 +29,8 @@ pub struct GyandootConfig { pub gateway: GatewaySection, #[serde(default)] pub browser: BrowserSection, + #[serde(default)] + pub identity: IdentitySection, } impl Default for GyandootConfig { @@ -40,6 +42,7 @@ impl Default for GyandootConfig { memory: MemorySection::default(), gateway: GatewaySection::default(), browser: BrowserSection::default(), + identity: IdentitySection::default(), } } } @@ -238,6 +241,38 @@ impl Default for BrowserSection { } } +/// `[identity]` section — agent and user identity for MemPalace onboarding. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct IdentitySection { + /// Agent's name (e.g. "GyanDoot", "Maya"). + #[serde(default)] + pub agent_name: String, + /// Agent's role / personality description. + #[serde(default)] + pub agent_role: String, + /// User's name (the human operating the agent). + #[serde(default)] + pub user_name: String, + /// User's role / relationship to the agent. + #[serde(default)] + pub user_role: String, + /// Whether identity onboarding has been completed. + #[serde(default)] + pub setup_complete: bool, +} + +impl Default for IdentitySection { + fn default() -> Self { + Self { + agent_name: String::new(), + agent_role: String::new(), + user_name: String::new(), + user_role: String::new(), + setup_complete: false, + } + } +} + // ─── Legacy / backward-compatible config types ────────────────────────── // These are re-exported under their original names so downstream crates // (gyandoot-agent, gyandoot-providers, etc.) continue to compile unchanged. @@ -256,6 +291,7 @@ pub struct AgentConfig { pub security: SecurityRootConfig, pub kanban: KanbanRootConfig, pub skills: SkillsRootConfig, + pub identity: IdentitySection, } impl Default for AgentConfig { @@ -272,6 +308,7 @@ impl Default for AgentConfig { security: SecurityRootConfig::default(), kanban: KanbanRootConfig::default(), skills: SkillsRootConfig::default(), + identity: IdentitySection::default(), } } } @@ -310,6 +347,7 @@ impl From for AgentConfig { security: SecurityRootConfig::default(), kanban: KanbanRootConfig::default(), skills: SkillsRootConfig::default(), + identity: cfg.identity, } } } diff --git a/crates/gyandoot-memory/src/palace.rs b/crates/gyandoot-memory/src/palace.rs index 5cc31ce..198801b 100644 --- a/crates/gyandoot-memory/src/palace.rs +++ b/crates/gyandoot-memory/src/palace.rs @@ -388,7 +388,55 @@ impl MemPalace { Ok(()) } - // ── Getters ───────────────────────────────────────────────────────── + // ── Dialogue / verbatim memory ───────────────────────────────────── + + /// Append a single message to the verbatim dialogue log. + /// + /// This is a line-by-line, word-by-word record of every message, + /// stored in `dialogue.md` within the wing directory. Each entry + /// is timestamped and tagged with role — no compression, no truncation. + pub async fn dialogue_append(&self, role: &str, content: &str) -> PalaceResult<()> { + let path = self.base_dir.join("dialogue.md"); + let timestamp = Utc::now().format("%Y-%m-%d %H:%M:%S%.3f").to_string(); + + use std::io::Write; + let mut file = std::fs::OpenOptions::new() + .create(true) + .append(true) + .open(&path)?; + + // Write a header line for this exchange + let header = format!("\n## [{timestamp}] **{role}**\n"); + file.write_all(header.as_bytes())?; + + // Write every single line of content verbatim + for line in content.lines() { + writeln!(file, "{}", line)?; + } + file.flush()?; + + debug!( + "Dialogue appended {role} message ({len}B) to dialogue.md", + len = content.len() + ); + Ok(()) + } + + /// Read the full dialogue log, up to max_chars (for context injection). + pub async fn dialogue_read(&self, max_chars: usize) -> PalaceResult { + let path = self.base_dir.join("dialogue.md"); + if !path.exists() { + return Ok(String::new()); + } + let content = std::fs::read_to_string(&path)?; + if content.len() <= max_chars { + Ok(content) + } else { + // Take the last max_chars chars (recent history) + let start = content.len().saturating_sub(max_chars); + Ok(content[start..].to_string()) + } + } /// Get a reference to the in-memory transcript store. pub fn transcript_store(&self) -> &TranscriptStore { diff --git a/crates/gyandoot-subagents/src/lib.rs b/crates/gyandoot-subagents/src/lib.rs index fdacfca..f90e82e 100644 --- a/crates/gyandoot-subagents/src/lib.rs +++ b/crates/gyandoot-subagents/src/lib.rs @@ -268,6 +268,7 @@ impl SubAgentWorker { security: gyandoot_core::SecurityRootConfig::default(), kanban: gyandoot_core::KanbanRootConfig::default(), skills: gyandoot_core::SkillsRootConfig::default(), + identity: gyandoot_core::IdentitySection::default(), }; // Try to create the agent loop. If the provider config isn't viable diff --git a/src/main.rs b/src/main.rs index 6e0ce06..a22858a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -321,6 +321,7 @@ fn load_and_convert_config(path: &std::path::Path) -> Result