Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 9 additions & 4 deletions crates/agents/src/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -261,8 +261,11 @@ fn append_identity_and_user_sections(
(Some(name), None) => parts.push(format!("Your name is {name}.")),
_ => {},
}
if let Some(theme) = id.theme.as_deref() {
parts.push(format!("Your theme: {theme}."));
if let Some(creature) = id.creature.as_deref() {
parts.push(format!("Your creature: {creature}."));
}
if let Some(vibe) = id.vibe.as_deref() {
parts.push(format!("Your vibe: {vibe}."));
}
if !parts.is_empty() {
prompt.push_str(&parts.join(" "));
Expand Down Expand Up @@ -719,7 +722,8 @@ mod tests {
let identity = AgentIdentity {
name: Some("Momo".into()),
emoji: Some("🦜".into()),
theme: Some("cheerful parrot".into()),
creature: Some("parrot".into()),
vibe: Some("cheerful".into()),
};
let user = UserProfile {
name: Some("Alice".into()),
Expand All @@ -740,7 +744,8 @@ mod tests {
None,
);
assert!(prompt.contains("Your name is Momo 🦜."));
assert!(prompt.contains("Your theme: cheerful parrot."));
assert!(prompt.contains("Your creature: parrot."));
assert!(prompt.contains("Your vibe: cheerful."));
assert!(prompt.contains("The user's name is Alice."));
// Default soul should be injected when soul is None.
assert!(prompt.contains("## Soul"));
Expand Down
7 changes: 5 additions & 2 deletions crates/chat/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1023,8 +1023,11 @@ fn load_prompt_persona() -> PromptPersona {
if file_identity.emoji.is_some() {
identity.emoji = file_identity.emoji;
}
if file_identity.theme.is_some() {
identity.theme = file_identity.theme;
if file_identity.creature.is_some() {
identity.creature = file_identity.creature;
}
if file_identity.vibe.is_some() {
identity.vibe = file_identity.vibe;
}
}
let mut user = config.user.clone();
Expand Down
14 changes: 7 additions & 7 deletions crates/config/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@ pub mod validate;

pub use {
loader::{
DEFAULT_SOUL, agents_path, apply_env_overrides, clear_config_dir, clear_data_dir,
config_dir, data_dir, discover_and_load, find_or_default_config_path,
find_user_global_config_file, heartbeat_path, home_dir, identity_path, load_agents_md,
load_heartbeat_md, load_identity, load_memory_md, load_soul, load_tools_md, load_user,
memory_path, save_config, save_identity, save_raw_config, save_soul, save_user,
set_config_dir, set_data_dir, soul_path, tools_path, update_config, user_global_config_dir,
user_global_config_dir_if_different, user_path,
DEFAULT_SOUL, agent_data_dir, agents_path, apply_env_overrides, clear_config_dir,
clear_data_dir, config_dir, data_dir, discover_and_load, extract_yaml_frontmatter,
find_or_default_config_path, find_user_global_config_file, heartbeat_path, home_dir,
identity_path, load_agents_md, load_heartbeat_md, load_identity, load_memory_md, load_soul,
load_tools_md, load_user, memory_path, save_config, save_identity, save_identity_for_agent,
save_raw_config, save_soul, save_user, set_config_dir, set_data_dir, soul_path, tools_path,
update_config, user_global_config_dir, user_global_config_dir_if_different, user_path,
},
schema::{
AgentIdentity, AuthConfig, ChatConfig, GeoLocation, MessageQueueMode, MoltisConfig,
Expand Down
81 changes: 70 additions & 11 deletions crates/config/src/loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -291,7 +291,11 @@ pub fn load_identity() -> Option<AgentIdentity> {
let content = std::fs::read_to_string(path).ok()?;
let frontmatter = extract_yaml_frontmatter(&content)?;
let identity = parse_identity_frontmatter(frontmatter);
if identity.name.is_none() && identity.emoji.is_none() && identity.theme.is_none() {
if identity.name.is_none()
&& identity.emoji.is_none()
&& identity.creature.is_none()
&& identity.vibe.is_none()
{
None
} else {
Some(identity)
Expand Down Expand Up @@ -448,8 +452,10 @@ pub fn save_soul(soul: Option<&str>) -> anyhow::Result<PathBuf> {
/// Persist identity values to `IDENTITY.md` using YAML frontmatter.
pub fn save_identity(identity: &AgentIdentity) -> anyhow::Result<PathBuf> {
let path = identity_path();
let has_values =
identity.name.is_some() || identity.emoji.is_some() || identity.theme.is_some();
let has_values = identity.name.is_some()
|| identity.emoji.is_some()
|| identity.creature.is_some()
|| identity.vibe.is_some();

if !has_values {
if path.exists() {
Expand All @@ -469,8 +475,11 @@ pub fn save_identity(identity: &AgentIdentity) -> anyhow::Result<PathBuf> {
if let Some(emoji) = identity.emoji.as_deref() {
yaml_lines.push(format!("emoji: {}", yaml_scalar(emoji)));
}
if let Some(theme) = identity.theme.as_deref() {
yaml_lines.push(format!("theme: {}", yaml_scalar(theme)));
if let Some(creature) = identity.creature.as_deref() {
yaml_lines.push(format!("creature: {}", yaml_scalar(creature)));
}
if let Some(vibe) = identity.vibe.as_deref() {
yaml_lines.push(format!("vibe: {}", yaml_scalar(vibe)));
}
let yaml = yaml_lines.join("\n");
let content = format!(
Expand All @@ -481,6 +490,52 @@ pub fn save_identity(identity: &AgentIdentity) -> anyhow::Result<PathBuf> {
Ok(path)
}

/// Return the workspace data directory for a named agent.
/// Non-main agents live under `data_dir()/agents/<id>/`.
pub fn agent_data_dir(agent_id: &str) -> PathBuf {
data_dir().join("agents").join(agent_id)
}

/// Persist identity values for a non-main agent into its workspace.
pub fn save_identity_for_agent(
agent_id: &str,
identity: &AgentIdentity,
) -> anyhow::Result<PathBuf> {
let dir = agent_data_dir(agent_id);
std::fs::create_dir_all(&dir)?;
let path = dir.join("IDENTITY.md");

let has_values = identity.name.is_some()
|| identity.emoji.is_some()
|| identity.creature.is_some()
|| identity.vibe.is_some();

if !has_values {
if path.exists() {
std::fs::remove_file(&path)?;
}
return Ok(path);
}

let mut yaml_lines = Vec::new();
if let Some(name) = identity.name.as_deref() {
yaml_lines.push(format!("name: {}", yaml_scalar(name)));
}
if let Some(emoji) = identity.emoji.as_deref() {
yaml_lines.push(format!("emoji: {}", yaml_scalar(emoji)));
}
if let Some(creature) = identity.creature.as_deref() {
yaml_lines.push(format!("creature: {}", yaml_scalar(creature)));
}
if let Some(vibe) = identity.vibe.as_deref() {
yaml_lines.push(format!("vibe: {}", yaml_scalar(vibe)));
}

let content = format!("---\n{}\n---\n", yaml_lines.join("\n"));
std::fs::write(&path, content)?;
Ok(path)
}

/// Persist user values to `USER.md` using YAML frontmatter.
pub fn save_user(user: &UserProfile) -> anyhow::Result<PathBuf> {
let path = user_path();
Expand Down Expand Up @@ -523,7 +578,7 @@ pub fn save_user(user: &UserProfile) -> anyhow::Result<PathBuf> {
Ok(path)
}

fn extract_yaml_frontmatter(content: &str) -> Option<&str> {
pub fn extract_yaml_frontmatter(content: &str) -> Option<&str> {
let trimmed = content.trim_start();
if !trimmed.starts_with("---") {
return None;
Expand Down Expand Up @@ -552,8 +607,9 @@ fn parse_identity_frontmatter(frontmatter: &str) -> AgentIdentity {
match key {
"name" => identity.name = Some(value.to_string()),
"emoji" => identity.emoji = Some(value.to_string()),
// Accept "creature" and "vibe" as backward-compat aliases for "theme".
"theme" | "creature" | "vibe" => identity.theme = Some(value.to_string()),
// Accept "theme" as backward-compat alias for "creature".
"creature" | "theme" => identity.creature = Some(value.to_string()),
"vibe" => identity.vibe = Some(value.to_string()),
_ => {},
}
}
Expand Down Expand Up @@ -1270,7 +1326,8 @@ name = "Rex"
let identity = AgentIdentity {
name: Some("Rex".to_string()),
emoji: Some("🐶".to_string()),
theme: Some("chill dog".to_string()),
creature: Some("golden retriever".to_string()),
vibe: Some("chill dog".to_string()),
};

let path = save_identity(&identity).expect("save identity");
Expand All @@ -1280,7 +1337,8 @@ name = "Rex"
let loaded = load_identity().expect("load identity");
assert_eq!(loaded.name.as_deref(), Some("Rex"));
assert_eq!(loaded.emoji.as_deref(), Some("🐶"), "raw file:\n{raw}");
assert_eq!(loaded.theme.as_deref(), Some("chill dog"));
assert_eq!(loaded.creature.as_deref(), Some("golden retriever"));
assert_eq!(loaded.vibe.as_deref(), Some("chill dog"));

clear_data_dir();
}
Expand All @@ -1294,7 +1352,8 @@ name = "Rex"
let seeded = AgentIdentity {
name: Some("Rex".to_string()),
emoji: None,
theme: None,
creature: None,
vibe: None,
};
let path = save_identity(&seeded).expect("seed identity");
assert!(path.exists());
Expand Down
14 changes: 9 additions & 5 deletions crates/config/src/schema.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,14 @@ use {
serde::{Deserialize, Serialize},
};

/// Agent identity (name, emoji, theme).
/// Agent identity (name, emoji, creature, vibe).
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct AgentIdentity {
pub name: Option<String>,
pub emoji: Option<String>,
pub theme: Option<String>,
pub creature: Option<String>,
pub vibe: Option<String>,
}

/// IANA timezone (e.g. `"Europe/Paris"`).
Expand Down Expand Up @@ -146,7 +147,8 @@ pub struct UserProfile {
pub struct ResolvedIdentity {
pub name: String,
pub emoji: Option<String>,
pub theme: Option<String>,
pub creature: Option<String>,
pub vibe: Option<String>,
pub soul: Option<String>,
pub user_name: Option<String>,
}
Expand All @@ -156,7 +158,8 @@ impl ResolvedIdentity {
Self {
name: cfg.identity.name.clone().unwrap_or_else(|| "moltis".into()),
emoji: cfg.identity.emoji.clone(),
theme: cfg.identity.theme.clone(),
creature: cfg.identity.creature.clone(),
vibe: cfg.identity.vibe.clone(),
soul: None,
user_name: cfg.user.name.clone(),
}
Expand All @@ -168,7 +171,8 @@ impl Default for ResolvedIdentity {
Self {
name: "moltis".into(),
emoji: None,
theme: None,
creature: None,
vibe: None,
soul: None,
user_name: None,
}
Expand Down
3 changes: 2 additions & 1 deletion crates/config/src/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,8 @@ fn build_schema_map() -> KnownKeys {
Struct(HashMap::from([
("name", Leaf),
("emoji", Leaf),
("theme", Leaf),
("creature", Leaf),
("vibe", Leaf),
])),
),
(
Expand Down
10 changes: 10 additions & 0 deletions crates/gateway/migrations/20260211110000_agents.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
CREATE TABLE IF NOT EXISTS agents (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
emoji TEXT,
creature TEXT,
vibe TEXT,
description TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
Loading
Loading