✨ Identity onboarding + Verbatim dialogue memory
- Added [identity] config section (agent_name, agent_role, user_name, user_role, setup_complete) to GyandootConfig and AgentConfig - Agent loop now injects identity into system prompt on startup - Agent loop injects past verbatim dialogue memory into system prompt - Added dialogue_append() / dialogue_read() to MemPalace - Word-by-word, line-by-line verbatim record in dialogue.md - No compression, no truncation — every message preserved - Agent loop writes to BOTH daily transcript AND verbatim dialogue on every turn - All config paths pass identity through (flat converter, subagents)
This commit is contained in:
@@ -159,8 +159,59 @@ impl AgentLoop {
|
|||||||
let _guard = span.enter();
|
let _guard = span.enter();
|
||||||
info!("Agent loop started (interactive)");
|
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::<Vec<_>>();
|
||||||
|
(
|
||||||
|
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<Message> =
|
let mut messages: Vec<Message> =
|
||||||
vec![Message::new(MessageRole::System, &self.config.system_prompt)];
|
vec![Message::new(MessageRole::System, &system_prompt)];
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
// 1. Read user input
|
// 1. Read user input
|
||||||
@@ -174,19 +225,26 @@ impl AgentLoop {
|
|||||||
|
|
||||||
// 3. Sanitize
|
// 3. Sanitize
|
||||||
let clean_input = self.sanitizer.sanitize_input(&user_input, &gyandoot_security::sanitizer::SanitizerConfig::default());
|
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)
|
// 4. Run the core turn (LLM + tool execution)
|
||||||
let new_messages = self.execute_turn(messages).await?;
|
let new_messages = self.execute_turn(messages).await?;
|
||||||
messages = new_messages;
|
messages = new_messages;
|
||||||
|
|
||||||
// 5. Store conversation turn in memory
|
// 5. Store conversation turn in memory (transcript + verbatim dialogue)
|
||||||
{
|
{
|
||||||
let memory = self.memory.lock().await;
|
let memory = self.memory.lock().await;
|
||||||
|
// Daily transcript
|
||||||
memory
|
memory
|
||||||
.transcript_append("user", &user_input)
|
.transcript_append("user", &user_input)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_else(|e| warn!("Failed to store user message: {e}"));
|
.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
|
// Store last assistant response
|
||||||
if let Some(last) = messages.last() {
|
if let Some(last) = messages.last() {
|
||||||
if last.role == MessageRole::Assistant {
|
if last.role == MessageRole::Assistant {
|
||||||
@@ -194,6 +252,10 @@ impl AgentLoop {
|
|||||||
.transcript_append("assistant", &last.content)
|
.transcript_append("assistant", &last.content)
|
||||||
.await
|
.await
|
||||||
.unwrap_or_else(|e| warn!("Failed to store assistant message: {e}"));
|
.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}"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,6 +29,8 @@ pub struct GyandootConfig {
|
|||||||
pub gateway: GatewaySection,
|
pub gateway: GatewaySection,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub browser: BrowserSection,
|
pub browser: BrowserSection,
|
||||||
|
#[serde(default)]
|
||||||
|
pub identity: IdentitySection,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for GyandootConfig {
|
impl Default for GyandootConfig {
|
||||||
@@ -40,6 +42,7 @@ impl Default for GyandootConfig {
|
|||||||
memory: MemorySection::default(),
|
memory: MemorySection::default(),
|
||||||
gateway: GatewaySection::default(),
|
gateway: GatewaySection::default(),
|
||||||
browser: BrowserSection::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 ──────────────────────────
|
// ─── Legacy / backward-compatible config types ──────────────────────────
|
||||||
// These are re-exported under their original names so downstream crates
|
// These are re-exported under their original names so downstream crates
|
||||||
// (gyandoot-agent, gyandoot-providers, etc.) continue to compile unchanged.
|
// (gyandoot-agent, gyandoot-providers, etc.) continue to compile unchanged.
|
||||||
@@ -256,6 +291,7 @@ pub struct AgentConfig {
|
|||||||
pub security: SecurityRootConfig,
|
pub security: SecurityRootConfig,
|
||||||
pub kanban: KanbanRootConfig,
|
pub kanban: KanbanRootConfig,
|
||||||
pub skills: SkillsRootConfig,
|
pub skills: SkillsRootConfig,
|
||||||
|
pub identity: IdentitySection,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AgentConfig {
|
impl Default for AgentConfig {
|
||||||
@@ -272,6 +308,7 @@ impl Default for AgentConfig {
|
|||||||
security: SecurityRootConfig::default(),
|
security: SecurityRootConfig::default(),
|
||||||
kanban: KanbanRootConfig::default(),
|
kanban: KanbanRootConfig::default(),
|
||||||
skills: SkillsRootConfig::default(),
|
skills: SkillsRootConfig::default(),
|
||||||
|
identity: IdentitySection::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -310,6 +347,7 @@ impl From<GyandootConfig> for AgentConfig {
|
|||||||
security: SecurityRootConfig::default(),
|
security: SecurityRootConfig::default(),
|
||||||
kanban: KanbanRootConfig::default(),
|
kanban: KanbanRootConfig::default(),
|
||||||
skills: SkillsRootConfig::default(),
|
skills: SkillsRootConfig::default(),
|
||||||
|
identity: cfg.identity,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -388,7 +388,55 @@ impl MemPalace {
|
|||||||
Ok(())
|
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<String> {
|
||||||
|
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.
|
/// Get a reference to the in-memory transcript store.
|
||||||
pub fn transcript_store(&self) -> &TranscriptStore {
|
pub fn transcript_store(&self) -> &TranscriptStore {
|
||||||
|
|||||||
@@ -268,6 +268,7 @@ impl SubAgentWorker {
|
|||||||
security: gyandoot_core::SecurityRootConfig::default(),
|
security: gyandoot_core::SecurityRootConfig::default(),
|
||||||
kanban: gyandoot_core::KanbanRootConfig::default(),
|
kanban: gyandoot_core::KanbanRootConfig::default(),
|
||||||
skills: gyandoot_core::SkillsRootConfig::default(),
|
skills: gyandoot_core::SkillsRootConfig::default(),
|
||||||
|
identity: gyandoot_core::IdentitySection::default(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Try to create the agent loop. If the provider config isn't viable
|
// Try to create the agent loop. If the provider config isn't viable
|
||||||
|
|||||||
@@ -321,6 +321,7 @@ fn load_and_convert_config(path: &std::path::Path) -> Result<gyandoot_core::conf
|
|||||||
default: provider_name,
|
default: provider_name,
|
||||||
providers: providers_map,
|
providers: providers_map,
|
||||||
},
|
},
|
||||||
|
identity: gyandoot_core::config::IdentitySection::default(),
|
||||||
platform: gyandoot_core::config::PlatformSection::default(),
|
platform: gyandoot_core::config::PlatformSection::default(),
|
||||||
memory: gyandoot_core::config::MemorySection::default(),
|
memory: gyandoot_core::config::MemorySection::default(),
|
||||||
gateway: gyandoot_core::config::GatewaySection::default(),
|
gateway: gyandoot_core::config::GatewaySection::default(),
|
||||||
|
|||||||
Reference in New Issue
Block a user