use std::path::Path;
pub const PORTFOLIO_PALACE_ID: &str = "tm-manager-portfolio";
const MANAGER_DATA_SUBDIR: &str = "manager";
pub struct PortfolioPalace {
id: String,
available: bool,
reason: Option<String>,
#[cfg(feature = "manager-memory")]
memory: Option<PortfolioMemory>,
}
impl std::fmt::Debug for PortfolioPalace {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PortfolioPalace")
.field("id", &self.id)
.field("available", &self.available)
.field("reason", &self.reason)
.finish()
}
}
impl PortfolioPalace {
pub fn provision(framework_root: &Path) -> Self {
let id = PORTFOLIO_PALACE_ID.to_string();
#[cfg(feature = "manager-memory")]
{
let data_root = framework_root.join(MANAGER_DATA_SUBDIR).join("palace");
match PortfolioMemory::open(data_root) {
Ok(memory) => Self {
id,
available: true,
reason: None,
memory: Some(memory),
},
Err(e) => {
tracing::warn!(
"portfolio manager palace unavailable: {e}; palace features disabled"
);
Self {
id,
available: false,
reason: Some("palace open failed (see daemon logs)".to_string()),
memory: None,
}
}
}
}
#[cfg(not(feature = "manager-memory"))]
{
let _ = (framework_root, MANAGER_DATA_SUBDIR);
Self {
id,
available: false,
reason: Some(
"portfolio memory engine not compiled (build with --features \
manager-memory to enable)"
.to_string(),
),
}
}
}
pub fn id(&self) -> &str {
&self.id
}
pub fn is_available(&self) -> bool {
self.available
}
pub fn unavailable_reason(&self) -> Option<&str> {
self.reason.as_deref()
}
pub async fn record_chat_turn(&self, conversation_key: &str, user: &str, assistant: &str) {
#[cfg(feature = "manager-memory")]
{
if let Some(mem) = self.memory() {
let text = format!(
"portfolio chat turn (conversation {conversation_key})\n\
user: {user}\nmanager: {assistant}"
);
let tags = vec![
"portfolio-chat".to_string(),
format!("conversation:{conversation_key}"),
];
if let Err(e) = mem.remember(text, tags).await {
tracing::warn!("portfolio chat-turn persistence failed: {e}");
}
}
}
#[cfg(not(feature = "manager-memory"))]
{
let _ = (conversation_key, user, assistant);
}
}
#[cfg(feature = "manager-memory")]
pub fn memory(&self) -> Option<&PortfolioMemory> {
self.memory.as_ref()
}
}
#[cfg(feature = "manager-memory")]
#[derive(Debug, thiserror::Error)]
pub enum PortfolioMemoryError {
#[error("portfolio palace '{palace}' unavailable: {source}")]
Palace {
palace: String,
source: anyhow::Error,
},
#[error("portfolio memory operation '{op}' failed: {source}")]
Operation {
op: &'static str,
source: anyhow::Error,
},
}
#[cfg(feature = "manager-memory")]
#[derive(Clone)]
pub struct PortfolioMemory {
registry: trusty_common::memory_core::registry::PalaceRegistry,
data_root: std::path::PathBuf,
palace_id: trusty_common::memory_core::palace::PalaceId,
}
#[cfg(feature = "manager-memory")]
mod live {
use super::{PORTFOLIO_PALACE_ID, PortfolioMemory, PortfolioMemoryError};
use std::path::PathBuf;
use std::sync::Arc;
use chrono::Utc;
use trusty_common::memory_core::palace::{Palace, PalaceId, RoomType};
use trusty_common::memory_core::registry::PalaceRegistry;
use trusty_common::memory_core::retrieval::{
PalaceHandle, RecallResult, RememberOptions, recall_with_default_embedder,
};
pub type PortfolioMemoryResult<T> = std::result::Result<T, PortfolioMemoryError>;
const PORTFOLIO_DEFAULT_IMPORTANCE: f32 = 0.7;
const PORTFOLIO_RECALL_TOP_K: usize = 8;
impl PortfolioMemory {
pub fn open(data_root: impl Into<PathBuf>) -> PortfolioMemoryResult<Self> {
let data_root = data_root.into();
let palace_id = PalaceId::new(PORTFOLIO_PALACE_ID.to_string());
let registry = PalaceRegistry::open(&data_root).map_err(|source| {
PortfolioMemoryError::Palace {
palace: palace_id.to_string(),
source,
}
})?;
let me = Self {
registry,
data_root,
palace_id,
};
me.ensure_palace()?;
Ok(me)
}
pub fn palace_id(&self) -> &PalaceId {
&self.palace_id
}
fn ensure_palace(&self) -> PortfolioMemoryResult<Arc<PalaceHandle>> {
if let Some(handle) = self.registry.get(&self.palace_id) {
return Ok(handle);
}
if let Ok(handle) = self.registry.open_palace(&self.data_root, &self.palace_id) {
return Ok(handle);
}
let palace = Palace {
id: self.palace_id.clone(),
name: "tm manager (portfolio)".to_string(),
description: Some(
"Portfolio-scoped tm manager palace (DOC-36 §3.4): digest \
history, escalation dispositions, and portfolio chat turns — \
manager-level observations about the portfolio, never \
project-scoped facts."
.to_string(),
),
created_at: Utc::now(),
data_dir: self.data_root.join(self.palace_id.as_str()),
};
self.registry
.create_palace(&self.data_root, palace)
.map_err(|source| PortfolioMemoryError::Palace {
palace: self.palace_id.to_string(),
source,
})
}
pub async fn remember(
&self,
text: impl Into<String>,
tags: Vec<String>,
) -> PortfolioMemoryResult<String> {
let handle = self.ensure_palace()?;
handle
.remember_with_options(
text.into(),
RoomType::General,
tags,
PORTFOLIO_DEFAULT_IMPORTANCE,
RememberOptions::note(),
)
.await
.map(|id| id.to_string())
.map_err(|source| PortfolioMemoryError::Operation {
op: "remember",
source,
})
}
pub async fn recall(&self, query: &str) -> PortfolioMemoryResult<Vec<RecallResult>> {
let handle = self.ensure_palace()?;
recall_with_default_embedder(&handle, query, PORTFOLIO_RECALL_TOP_K)
.await
.map_err(|source| PortfolioMemoryError::Operation {
op: "recall",
source,
})
}
pub fn persisted_palace_count(&self) -> PortfolioMemoryResult<usize> {
PalaceRegistry::list_palaces(&self.data_root)
.map(|palaces| palaces.len())
.map_err(|source| PortfolioMemoryError::Palace {
palace: self.palace_id.to_string(),
source,
})
}
}
}
#[cfg(test)]
#[path = "memory_tests.rs"]
mod tests;