conversation seeding + memfs frontmatter fixes
API-created conversations (shell /agent, /new) never got a system prompt — the agent booted amnesiac with no constitution, memories, or skills. Prompt seeding now lives on SouveraineServer and every entry point shares it. Memory frontmatter: stop serializing None as 'key: null', round-trip unknown keys instead of destroying them, merge agent-supplied frontmatter instead of nesting a second block in the body, and make reads tolerant so a nonconforming file can still be read and healed.
This commit is contained in:
parent
856c6e5576
commit
dc9fe900ec
4 changed files with 227 additions and 156 deletions
|
|
@ -137,6 +137,14 @@ pub async fn create_conversation(
|
|||
|
||||
let conversation_id = server.sessions.create(&request.agent_id);
|
||||
|
||||
// Seed the full system prompt (constitution, base memories, skills) —
|
||||
// without this the agent boots amnesiac on shell-created conversations.
|
||||
server.seed_conversation_system_prompt(&request.agent_id, &conversation_id).await
|
||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse {
|
||||
error: "system_prompt_seed_failed".to_string(),
|
||||
message: e.to_string(),
|
||||
})))?;
|
||||
|
||||
let conversation = Conversation {
|
||||
id: conversation_id,
|
||||
agent_id: request.agent_id,
|
||||
|
|
|
|||
|
|
@ -153,22 +153,6 @@ impl LocalBackend {
|
|||
self.server.shutdown_sensoria().await;
|
||||
}
|
||||
|
||||
/// Build the greeting line describing the agent's current visual state
|
||||
/// (atmosphere and outfit). Returns `None` when no atmosphere is set in
|
||||
/// config (fresh init, no state to report).
|
||||
async fn build_visual_greeting(&self) -> Option<String> {
|
||||
let config = self.server.app_config.read().await;
|
||||
let atm = config.presence.atmosphere.as_deref()?;
|
||||
if atm.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let display = atm.replace('_', " ");
|
||||
let outfit = config.presence.outfit.as_deref().unwrap_or("default");
|
||||
Some(format!(
|
||||
"\n\nYour current atmosphere is {}, wearing the \"{}\" outfit.",
|
||||
display, outfit
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
|
|
@ -197,48 +181,7 @@ impl Backend for LocalBackend {
|
|||
tracing::warn!(agent = %agent_id, "instance registration failed: {}", e);
|
||||
}
|
||||
|
||||
let memory_root = self.server.agents.memory_root(agent_id);
|
||||
let subconscious_root = self.server.agents.subconscious_memory_root(agent_id);
|
||||
let (bundled, user, agent_memfs, project) =
|
||||
crate::core::skills::default_discovery_paths(Some(memory_root.clone()));
|
||||
let skills = crate::core::skills::discover(
|
||||
bundled.as_deref(),
|
||||
user.as_deref(),
|
||||
agent_memfs.as_deref(),
|
||||
project.as_deref(),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let platform_prompt = self.server.app_config.read().await
|
||||
.agent.system_prompt.clone();
|
||||
let system_prompt = crate::core::prompt::build_system_prompt_full(
|
||||
&memory_root,
|
||||
Some(&subconscious_root),
|
||||
platform_prompt.as_deref(),
|
||||
Some(&skills),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Append visual state greeting.
|
||||
let greeting_extra = self.build_visual_greeting().await;
|
||||
let system_prompt = if let Some(extra) = greeting_extra {
|
||||
format!("{}{}", system_prompt, extra)
|
||||
} else {
|
||||
system_prompt
|
||||
};
|
||||
|
||||
self.server.sessions.add_message(
|
||||
&conv_id,
|
||||
ConversationMessage {
|
||||
role: crate::core::session::MessageRole::System,
|
||||
blocks: vec![crate::core::session::ContentBlock::Text {
|
||||
text: system_prompt,
|
||||
}],
|
||||
usage: None,
|
||||
timestamp: Some(chrono::Utc::now()),
|
||||
},
|
||||
)?;
|
||||
self.server.seed_conversation_system_prompt(agent_id, &conv_id).await?;
|
||||
|
||||
Ok(conv_id)
|
||||
}
|
||||
|
|
@ -315,50 +258,7 @@ impl Backend for LocalBackend {
|
|||
let conv_id = self.server.sessions.create(agent_id);
|
||||
|
||||
// Build system prompt from the agent's memfs and inject as first message
|
||||
let memory_root = self.server.agents.memory_root(agent_id);
|
||||
let subconscious_root = self.server.agents.subconscious_memory_root(agent_id);
|
||||
|
||||
// Discover skills from all 4 tiers
|
||||
let (bundled, user, agent_memfs, project) =
|
||||
crate::core::skills::default_discovery_paths(Some(memory_root.clone()));
|
||||
let skills = crate::core::skills::discover(
|
||||
bundled.as_deref(),
|
||||
user.as_deref(),
|
||||
agent_memfs.as_deref(),
|
||||
project.as_deref(),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let platform_prompt = self.server.app_config.read().await
|
||||
.agent.system_prompt.clone();
|
||||
let system_prompt = crate::core::prompt::build_system_prompt_full(
|
||||
&memory_root,
|
||||
Some(&subconscious_root),
|
||||
platform_prompt.as_deref(),
|
||||
Some(&skills),
|
||||
)
|
||||
.await;
|
||||
|
||||
// Append visual state greeting.
|
||||
let greeting_extra = self.build_visual_greeting().await;
|
||||
let system_prompt = if let Some(extra) = greeting_extra {
|
||||
format!("{}{}", system_prompt, extra)
|
||||
} else {
|
||||
system_prompt
|
||||
};
|
||||
|
||||
self.server.sessions.add_message(
|
||||
&conv_id,
|
||||
ConversationMessage {
|
||||
role: crate::core::session::MessageRole::System,
|
||||
blocks: vec![crate::core::session::ContentBlock::Text {
|
||||
text: system_prompt,
|
||||
}],
|
||||
usage: None,
|
||||
timestamp: Some(chrono::Utc::now()),
|
||||
},
|
||||
)?;
|
||||
self.server.seed_conversation_system_prompt(agent_id, &conv_id).await?;
|
||||
|
||||
Ok(conv_id)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,15 +40,24 @@ pub struct MemoryFile {
|
|||
}
|
||||
|
||||
/// YAML frontmatter fields for a memory file.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
///
|
||||
/// `None` fields are omitted on render — never serialized as `key: null`
|
||||
/// (nulls in frontmatter read as noise and confused agents into copying
|
||||
/// the pattern). Unknown keys agents add (`name:`, `metadata:`, …) are
|
||||
/// captured in `extra` and round-tripped verbatim instead of being
|
||||
/// destroyed on the next rewrite.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct MemoryFrontmatter {
|
||||
/// Human-readable description of this file's purpose (required).
|
||||
/// Human-readable description of this file's purpose (required on
|
||||
/// tool-path create; tolerated empty on read so nonconforming files
|
||||
/// stay reachable and can be healed by a rewrite).
|
||||
#[serde(default)]
|
||||
pub description: String,
|
||||
/// If "true", the file cannot be modified via the memory tool.
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub read_only: Option<String>,
|
||||
/// Optional tags for categorization.
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub tags: Option<Vec<String>>,
|
||||
/// Optional max body size in characters. Writes/appends that would exceed
|
||||
/// this length are rejected. Closes a gap where upstream memfs write path
|
||||
|
|
@ -57,8 +66,11 @@ pub struct MemoryFrontmatter {
|
|||
/// Units are characters, not tokens — cheap to enforce without a tokenizer.
|
||||
/// Best-practice default for system/ files: 4_000 characters
|
||||
/// (~1k tokens). For journal/, leave unset.
|
||||
#[serde(default)]
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub limit: Option<usize>,
|
||||
/// Any other frontmatter keys, preserved across rewrites.
|
||||
#[serde(flatten, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
|
||||
pub extra: std::collections::BTreeMap<String, serde_yaml::Value>,
|
||||
}
|
||||
|
||||
/// Status of the memory repo.
|
||||
|
|
@ -176,9 +188,9 @@ impl MemoryRepo {
|
|||
let persona_content = render_frontmatter(
|
||||
&MemoryFrontmatter {
|
||||
description: "Agent identity, voice, principles".to_string(),
|
||||
read_only: None,
|
||||
tags: Some(vec!["system".to_string()]),
|
||||
limit: Some(4_000),
|
||||
..Default::default()
|
||||
},
|
||||
"# Identity\n\nAgent identity and core principles go here.\n",
|
||||
);
|
||||
|
|
@ -189,9 +201,8 @@ impl MemoryRepo {
|
|||
let state_content = render_frontmatter(
|
||||
&MemoryFrontmatter {
|
||||
description: "Current execution state and phase tracking".to_string(),
|
||||
read_only: None,
|
||||
tags: None,
|
||||
limit: Some(2_000),
|
||||
..Default::default()
|
||||
},
|
||||
"phase: idle\ncurrent_unit: none\n",
|
||||
);
|
||||
|
|
@ -299,8 +310,11 @@ impl MemoryRepo {
|
|||
.context("creating parent directories")?;
|
||||
}
|
||||
|
||||
// Agent-supplied frontmatter in the content is merged, not nested.
|
||||
let (supplied, body) = split_supplied_frontmatter(body);
|
||||
|
||||
// Get existing frontmatter or use default
|
||||
let frontmatter = if path.exists() {
|
||||
let base = if path.exists() {
|
||||
let existing = tokio::fs::read_to_string(&path).await?;
|
||||
let parsed = parse_memory_file(&existing)?;
|
||||
if parsed.frontmatter.read_only.as_deref() == Some("true") {
|
||||
|
|
@ -310,11 +324,13 @@ impl MemoryRepo {
|
|||
} else {
|
||||
MemoryFrontmatter {
|
||||
description: format!("Memory file: {}", label),
|
||||
read_only: None,
|
||||
tags: None,
|
||||
limit: None,
|
||||
..Default::default()
|
||||
}
|
||||
};
|
||||
let frontmatter = match supplied {
|
||||
Some(fm) => merge_frontmatter(base, fm),
|
||||
None => base,
|
||||
};
|
||||
|
||||
// Enforce frontmatter `limit:` (LET-8133 closure).
|
||||
if let Some(max) = frontmatter.limit {
|
||||
|
|
@ -344,12 +360,19 @@ impl MemoryRepo {
|
|||
pub async fn append(&self, label: &str, content: &str) -> Result<()> {
|
||||
let path = self.resolve_path(label);
|
||||
|
||||
// Frontmatter never belongs mid-file; merge it instead of nesting.
|
||||
let (supplied, content) = split_supplied_frontmatter(content);
|
||||
|
||||
if path.exists() {
|
||||
let existing = tokio::fs::read_to_string(&path).await?;
|
||||
let parsed = parse_memory_file(&existing)?;
|
||||
if parsed.frontmatter.read_only.as_deref() == Some("true") {
|
||||
return Err(anyhow!("memory file is read_only: {}", label));
|
||||
}
|
||||
let frontmatter = match supplied {
|
||||
Some(fm) => merge_frontmatter(parsed.frontmatter, fm),
|
||||
None => parsed.frontmatter,
|
||||
};
|
||||
// Write back body + new content, preserving frontmatter
|
||||
let new_body = if parsed.body.is_empty() {
|
||||
content.to_string()
|
||||
|
|
@ -357,7 +380,7 @@ impl MemoryRepo {
|
|||
format!("{}\n{}", parsed.body.trim_end(), content)
|
||||
};
|
||||
// Enforce frontmatter `limit:` (LET-8133 closure).
|
||||
if let Some(max) = parsed.frontmatter.limit {
|
||||
if let Some(max) = frontmatter.limit {
|
||||
if new_body.chars().count() > max {
|
||||
return Err(anyhow!(
|
||||
"memory append rejected: body would be {} chars, limit is {} (file: {})",
|
||||
|
|
@ -367,7 +390,7 @@ impl MemoryRepo {
|
|||
));
|
||||
}
|
||||
}
|
||||
let rendered = render_frontmatter(&parsed.frontmatter, &new_body);
|
||||
let rendered = render_frontmatter(&frontmatter, &new_body);
|
||||
tokio::fs::write(&path, &rendered).await?;
|
||||
|
||||
if self.auto_commit {
|
||||
|
|
@ -376,13 +399,14 @@ impl MemoryRepo {
|
|||
return Ok(());
|
||||
};
|
||||
|
||||
// File doesn't exist — create it with default frontmatter
|
||||
let frontmatter = MemoryFrontmatter {
|
||||
description: format!("Memory file: {}", label),
|
||||
read_only: None,
|
||||
tags: None,
|
||||
limit: None,
|
||||
};
|
||||
// File doesn't exist — create it with supplied or default frontmatter
|
||||
let frontmatter = merge_frontmatter(
|
||||
MemoryFrontmatter {
|
||||
description: format!("Memory file: {}", label),
|
||||
..Default::default()
|
||||
},
|
||||
supplied.unwrap_or_default(),
|
||||
);
|
||||
let rendered = render_frontmatter(&frontmatter, content);
|
||||
tokio::fs::write(&path, &rendered).await?;
|
||||
|
||||
|
|
@ -676,36 +700,82 @@ impl MemoryRepo {
|
|||
/// ---
|
||||
/// Body content here...
|
||||
/// ```
|
||||
pub fn parse_memory_file(content: &str) -> Result<MemoryFile> {
|
||||
// Find the frontmatter delimiters
|
||||
/// Split a leading `--- … ---` frontmatter block off `content`.
|
||||
/// Returns `(frontmatter_yaml, body)` or `None` when there is no block.
|
||||
fn split_frontmatter_block(content: &str) -> Option<(&str, &str)> {
|
||||
let content = content.trim_start();
|
||||
if !content.starts_with("---") {
|
||||
return Err(anyhow!(
|
||||
"memory file is missing required frontmatter (--- delimiters)"
|
||||
));
|
||||
return None;
|
||||
}
|
||||
|
||||
// Find closing `---`
|
||||
let after_first = &content[3..];
|
||||
let end_idx = after_first.find("\n---")
|
||||
.or_else(|| after_first.find("\r\n---"))
|
||||
.ok_or_else(|| anyhow!("memory file frontmatter has no closing ---"))?;
|
||||
let end_idx = after_first
|
||||
.find("\n---")
|
||||
.or_else(|| after_first.find("\r\n---"))?;
|
||||
let frontmatter_text = after_first[..end_idx].trim();
|
||||
let body_start = 3 + end_idx + 4; // opening --- + yaml + \n---
|
||||
Some((frontmatter_text, content[body_start..].trim_start_matches(['\r', '\n'])))
|
||||
}
|
||||
|
||||
let frontmatter_text = &after_first[..end_idx].trim();
|
||||
let body_start = 3 + end_idx + 4; // 3 for opening --- + end_idx + 4 for \n---
|
||||
let body = content[body_start..].trim().to_string();
|
||||
/// Parse a memory file. Tolerant on read: a file with no frontmatter, or
|
||||
/// frontmatter that fails YAML parsing, comes back with the whole content
|
||||
/// as body and default (empty-description) frontmatter — a file that
|
||||
/// exists must always be readable through the tool, otherwise the agent
|
||||
/// can never heal it. Strictness (description required) belongs to the
|
||||
/// write path, not here.
|
||||
pub fn parse_memory_file(content: &str) -> Result<MemoryFile> {
|
||||
let Some((frontmatter_text, body)) = split_frontmatter_block(content) else {
|
||||
return Ok(MemoryFile {
|
||||
frontmatter: MemoryFrontmatter::default(),
|
||||
body: content.trim().to_string(),
|
||||
});
|
||||
};
|
||||
|
||||
// Parse YAML frontmatter
|
||||
let frontmatter: MemoryFrontmatter = serde_yaml::from_str(frontmatter_text)
|
||||
.context("parsing memory file frontmatter")?;
|
||||
|
||||
if frontmatter.description.trim().is_empty() {
|
||||
return Err(anyhow!(
|
||||
"memory file frontmatter is missing required 'description' field"
|
||||
));
|
||||
match serde_yaml::from_str::<MemoryFrontmatter>(frontmatter_text) {
|
||||
Ok(frontmatter) => Ok(MemoryFile {
|
||||
frontmatter,
|
||||
body: body.trim().to_string(),
|
||||
}),
|
||||
// Malformed YAML: keep the raw text intact as body so a rewrite
|
||||
// cannot silently destroy whatever the block was trying to say.
|
||||
Err(_) => Ok(MemoryFile {
|
||||
frontmatter: MemoryFrontmatter::default(),
|
||||
body: content.trim().to_string(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(MemoryFile { frontmatter, body })
|
||||
/// Agents sometimes include their own frontmatter in write/append content
|
||||
/// despite the "body only" contract. Instead of nesting a second `---`
|
||||
/// block inside the body (the persona.md failure mode), honor it: parse
|
||||
/// it off and merge its fields. Content whose leading block is not valid
|
||||
/// YAML is left untouched — never destroy what we cannot parse.
|
||||
fn split_supplied_frontmatter(content: &str) -> (Option<MemoryFrontmatter>, &str) {
|
||||
if let Some((frontmatter_text, body)) = split_frontmatter_block(content) {
|
||||
if let Ok(fm) = serde_yaml::from_str::<MemoryFrontmatter>(frontmatter_text) {
|
||||
return (Some(fm), body);
|
||||
}
|
||||
}
|
||||
(None, content)
|
||||
}
|
||||
|
||||
/// Overlay agent-supplied frontmatter onto the file's existing (or
|
||||
/// default) frontmatter: supplied fields win where set, everything else
|
||||
/// is preserved. `read_only` is deliberately NOT overridable from
|
||||
/// supplied content — clearing it requires the explicit tool path.
|
||||
fn merge_frontmatter(base: MemoryFrontmatter, supplied: MemoryFrontmatter) -> MemoryFrontmatter {
|
||||
let mut extra = base.extra;
|
||||
extra.extend(supplied.extra);
|
||||
MemoryFrontmatter {
|
||||
description: if supplied.description.trim().is_empty() {
|
||||
base.description
|
||||
} else {
|
||||
supplied.description
|
||||
},
|
||||
read_only: base.read_only,
|
||||
tags: supplied.tags.or(base.tags),
|
||||
limit: supplied.limit.or(base.limit),
|
||||
extra,
|
||||
}
|
||||
}
|
||||
|
||||
/// Render frontmatter + body into a complete memory file.
|
||||
|
|
@ -827,10 +897,10 @@ pub async fn execute_memory_command_with_context(
|
|||
}
|
||||
MemoryCommand::Read { path } => {
|
||||
let file = repo.read(path).await?;
|
||||
Ok(format!(
|
||||
"---\ndescription: {}\n---\n{}",
|
||||
file.frontmatter.description, file.body
|
||||
))
|
||||
// Normalized render: full frontmatter (description, tags,
|
||||
// limit, extras — no nulls), so what the agent reads matches
|
||||
// what a rewrite would produce.
|
||||
Ok(render_frontmatter(&file.frontmatter, &file.body))
|
||||
}
|
||||
MemoryCommand::Write { path, content } => {
|
||||
repo.write(path, content).await?;
|
||||
|
|
@ -1079,17 +1149,47 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_parse_missing_frontmatter() {
|
||||
// Tolerant read: no frontmatter means the whole content is body.
|
||||
let content = "Hello world without frontmatter";
|
||||
assert!(parse_memory_file(content).is_err());
|
||||
let file = parse_memory_file(content).unwrap();
|
||||
assert_eq!(file.body, content);
|
||||
assert!(file.frontmatter.description.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_omits_none_fields() {
|
||||
let fm = MemoryFrontmatter {
|
||||
description: "Bare defaults".to_string(),
|
||||
..Default::default()
|
||||
};
|
||||
let rendered = render_frontmatter(&fm, "Body");
|
||||
assert!(!rendered.contains("null"), "None fields must be omitted: {rendered}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extra_keys_roundtrip() {
|
||||
let content = "---\ndescription: Has extras\nname: my-slug\nkind: feedback\n---\nBody";
|
||||
let file = parse_memory_file(content).unwrap();
|
||||
assert_eq!(file.frontmatter.extra.len(), 2);
|
||||
let rendered = render_frontmatter(&file.frontmatter, &file.body);
|
||||
assert!(rendered.contains("name: my-slug"));
|
||||
assert!(rendered.contains("kind: feedback"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_supplied_frontmatter_not_nested() {
|
||||
let content = "---\ndescription: Supplied\n---\nActual body";
|
||||
let (fm, body) = split_supplied_frontmatter(content);
|
||||
assert_eq!(fm.unwrap().description, "Supplied");
|
||||
assert_eq!(body, "Actual body");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_and_parse_roundtrip() {
|
||||
let fm = MemoryFrontmatter {
|
||||
description: "Roundtrip test".to_string(),
|
||||
read_only: None,
|
||||
tags: Some(vec!["test".to_string()]),
|
||||
limit: None,
|
||||
..Default::default()
|
||||
};
|
||||
let rendered = render_frontmatter(&fm, "Body content");
|
||||
let parsed = parse_memory_file(&rendered).unwrap();
|
||||
|
|
@ -1179,8 +1279,7 @@ mod tests {
|
|||
let fm = MemoryFrontmatter {
|
||||
description: "Read-only test".to_string(),
|
||||
read_only: Some("true".to_string()),
|
||||
tags: None,
|
||||
limit: None,
|
||||
..Default::default()
|
||||
};
|
||||
let content = render_frontmatter(&fm, "This is read-only");
|
||||
let path = repo.root().join("test/readonly.md");
|
||||
|
|
|
|||
|
|
@ -361,6 +361,70 @@ impl SouveraineServer {
|
|||
})
|
||||
}
|
||||
|
||||
/// Seed a freshly created conversation with the agent's full system
|
||||
/// prompt — constitution/platform prompt, memfs base memories,
|
||||
/// subconscious window, skills, visual state. Every conversation
|
||||
/// entry point (TUI backend, HTTP API, sensoria) must pass through
|
||||
/// here: a session without this message boots the agent amnesiac.
|
||||
pub async fn seed_conversation_system_prompt(
|
||||
&self,
|
||||
agent_id: &str,
|
||||
conversation_id: &str,
|
||||
) -> anyhow::Result<()> {
|
||||
let memory_root = self.agents.memory_root(agent_id);
|
||||
let subconscious_root = self.agents.subconscious_memory_root(agent_id);
|
||||
|
||||
let (bundled, user, agent_memfs, project) =
|
||||
crate::core::skills::default_discovery_paths(Some(memory_root.clone()));
|
||||
let skills = crate::core::skills::discover(
|
||||
bundled.as_deref(),
|
||||
user.as_deref(),
|
||||
agent_memfs.as_deref(),
|
||||
project.as_deref(),
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let config = self.app_config.read().await;
|
||||
let platform_prompt = config.agent.system_prompt.clone();
|
||||
// Visual state greeting (atmosphere + outfit), when presence is set.
|
||||
let greeting_extra = config.presence.atmosphere.as_deref()
|
||||
.filter(|a| !a.is_empty())
|
||||
.map(|atm| {
|
||||
let display = atm.replace('_', " ");
|
||||
let outfit = config.presence.outfit.as_deref().unwrap_or("default");
|
||||
format!(
|
||||
"\n\nYour current atmosphere is {}, wearing the \"{}\" outfit.",
|
||||
display, outfit
|
||||
)
|
||||
});
|
||||
drop(config);
|
||||
|
||||
let mut system_prompt = crate::core::prompt::build_system_prompt_full(
|
||||
&memory_root,
|
||||
Some(&subconscious_root),
|
||||
platform_prompt.as_deref(),
|
||||
Some(&skills),
|
||||
)
|
||||
.await;
|
||||
if let Some(extra) = greeting_extra {
|
||||
system_prompt.push_str(&extra);
|
||||
}
|
||||
|
||||
self.sessions.add_message(
|
||||
conversation_id,
|
||||
crate::core::session::ConversationMessage {
|
||||
role: crate::core::session::MessageRole::System,
|
||||
blocks: vec![crate::core::session::ContentBlock::Text {
|
||||
text: system_prompt,
|
||||
}],
|
||||
usage: None,
|
||||
timestamp: Some(chrono::Utc::now()),
|
||||
},
|
||||
)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Register a sensorium on the coordinator and spawn its run loop.
|
||||
///
|
||||
/// Each sensorium gets its own task, a shared EventBus subscription,
|
||||
|
|
|
|||
Loading…
Reference in a new issue