use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum StartupStatus {
Ready,
Degraded {
reason: String,
last_usable_wakeup: Option<SystemPromptBlock>,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SystemPromptResponse {
pub status: StartupStatus,
pub prompt_block: Option<SystemPromptBlock>,
}
impl SystemPromptResponse {
#[must_use]
pub fn ready(prompt_block: SystemPromptBlock) -> Self {
Self {
status: StartupStatus::Ready,
prompt_block: Some(prompt_block),
}
}
#[must_use]
pub fn degraded(reason: impl Into<String>) -> Self {
let reason = reason.into();
Self {
prompt_block: Some(SystemPromptBlock {
shape: StartupInjectionShape::CompactRenderedMarkdown,
markdown: render_degraded_startup_markdown(&reason),
}),
status: StartupStatus::Degraded {
reason,
last_usable_wakeup: None,
},
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StartupInjectionShape {
CompactRenderedMarkdown,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PrefetchStatus {
SkippedNoIntent,
Ready,
Failed { reason: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SyncTurnStatus {
Persisted,
Noop,
Failed { reason: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SessionEndStatus {
Triggered,
Noop,
AlreadyHandled,
Failed { reason: String },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WakeupPackSummary {
pub identity: String,
pub recent_state: String,
pub latest_capsule: Option<String>,
pub key_relations: Vec<String>,
pub unresolved_threads: Vec<String>,
pub generated_at: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RhythmTrigger {
pub name: String,
pub reason: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct StartupContextSnapshot {
pub laputa_state_root: Option<PathBuf>,
pub soul_markdown: Option<String>,
pub wakeup_markdown: Option<String>,
pub wakeup_pack: Option<WakeupPackSummary>,
pub rhythm_triggers: Vec<RhythmTrigger>,
pub memory_markdown: Option<String>,
}
impl StartupContextSnapshot {
pub fn into_system_prompt_block(self) -> Option<SystemPromptBlock> {
let markdown = self.render_compact_markdown();
if markdown.is_empty() {
None
} else {
Some(SystemPromptBlock {
shape: StartupInjectionShape::CompactRenderedMarkdown,
markdown,
})
}
}
fn render_compact_markdown(&self) -> String {
let mut sections = Vec::new();
if let Some(memory_markdown) = trimmed_markdown(self.memory_markdown.as_deref()) {
sections.push(memory_markdown.to_string());
}
if let Some(soul_markdown) = trimmed_markdown(self.soul_markdown.as_deref()) {
sections.push(format!("## Soul Projection\n{}", soul_markdown));
}
if let Some(wakeup_markdown) = trimmed_markdown(self.wakeup_markdown.as_deref()) {
sections.push(format!("## Wakeup Projection\n{}", wakeup_markdown));
} else if let Some(wakeup_pack) = &self.wakeup_pack {
sections.push(render_wakeup_pack_summary(wakeup_pack));
}
if !self.rhythm_triggers.is_empty() {
let triggers = self
.rhythm_triggers
.iter()
.map(|trigger| match trigger.reason.as_deref() {
Some(reason) if !reason.trim().is_empty() => {
format!("- {} — {}", trigger.name.trim(), reason.trim())
}
_ => format!("- {}", trigger.name.trim()),
})
.collect::<Vec<_>>()
.join("\n");
sections.push(format!("## Rhythm Signals\n{}", triggers));
}
sections.join("\n\n")
}
}
fn render_degraded_startup_markdown(reason: &str) -> String {
format!(
"## Memory Startup Status\n- status: degraded\n- reason: {}\n- last_usable_wakeup: omitted (no cache reuse)\n",
reason.trim()
)
}
fn trimmed_markdown(markdown: Option<&str>) -> Option<&str> {
let markdown = markdown?.trim();
if markdown.is_empty() {
None
} else {
Some(markdown)
}
}
fn render_wakeup_pack_summary(pack: &WakeupPackSummary) -> String {
let latest_capsule = pack.latest_capsule.as_deref().unwrap_or("None");
let key_relations = if pack.key_relations.is_empty() {
"- None".to_string()
} else {
pack.key_relations
.iter()
.map(|item| format!("- {}", item.trim()))
.collect::<Vec<_>>()
.join("\n")
};
let unresolved_threads = if pack.unresolved_threads.is_empty() {
"- None".to_string()
} else {
pack.unresolved_threads
.iter()
.map(|item| format!("- {}", item.trim()))
.collect::<Vec<_>>()
.join("\n")
};
let mut rendered = String::from("## Wakeup Summary");
if let Some(generated_at) = trimmed_markdown(pack.generated_at.as_deref()) {
rendered.push_str("\nGenerated: ");
rendered.push_str(generated_at);
}
rendered.push_str("\n\n### Identity\n");
rendered.push_str(pack.identity.trim());
rendered.push_str("\n\n### Recent State\n");
rendered.push_str(pack.recent_state.trim());
rendered.push_str("\n\n### Latest Capsule\n");
rendered.push_str(latest_capsule.trim());
rendered.push_str("\n\n### Key Relations\n");
rendered.push_str(&key_relations);
rendered.push_str("\n\n### Unresolved Threads\n");
rendered.push_str(&unresolved_threads);
rendered
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SystemPromptRequest {
pub workspace_root: PathBuf,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SystemPromptBlock {
pub shape: StartupInjectionShape,
pub markdown: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PrefetchRequest {
pub workspace_root: PathBuf,
pub intent: String,
pub current_room: Option<String>,
pub user_message: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PrefetchResponse {
pub status: PrefetchStatus,
pub prompt_block: Option<String>,
}
impl Default for PrefetchResponse {
fn default() -> Self {
Self {
status: PrefetchStatus::SkippedNoIntent,
prompt_block: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SyncTurnRequest {
pub workspace_root: PathBuf,
pub memory_update_markdown: Option<String>,
pub history_entry: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SyncTurnResponse {
pub status: SyncTurnStatus,
}
impl Default for SyncTurnResponse {
fn default() -> Self {
Self {
status: SyncTurnStatus::Noop,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SessionEndRequest {
pub workspace_root: PathBuf,
pub session_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SessionEndResponse {
pub status: SessionEndStatus,
}
impl Default for SessionEndResponse {
fn default() -> Self {
Self {
status: SessionEndStatus::Noop,
}
}
}
#[async_trait::async_trait]
pub trait MemoryProvider: Send + Sync {
fn system_prompt_block(
&self,
request: &SystemPromptRequest,
) -> crate::Result<SystemPromptResponse>;
async fn prefetch(&self, request: PrefetchRequest) -> crate::Result<PrefetchResponse>;
async fn sync_turn(&self, request: SyncTurnRequest) -> crate::Result<SyncTurnResponse>;
async fn on_session_end(
&self,
request: SessionEndRequest,
) -> crate::Result<SessionEndResponse>;
}
#[cfg(test)]
mod tests {
use super::*;
struct DummyProvider;
#[async_trait::async_trait]
impl MemoryProvider for DummyProvider {
fn system_prompt_block(
&self,
request: &SystemPromptRequest,
) -> crate::Result<SystemPromptResponse> {
Ok(SystemPromptResponse::ready(SystemPromptBlock {
shape: StartupInjectionShape::CompactRenderedMarkdown,
markdown: format!("workspace={}", request.workspace_root.display()),
}))
}
async fn prefetch(&self, request: PrefetchRequest) -> crate::Result<PrefetchResponse> {
Ok(PrefetchResponse {
status: PrefetchStatus::Ready,
prompt_block: Some(format!(
"intent={} room={}",
request.intent,
request.current_room.unwrap_or_else(|| "none".to_string())
)),
})
}
async fn sync_turn(&self, request: SyncTurnRequest) -> crate::Result<SyncTurnResponse> {
Ok(SyncTurnResponse {
status: if request.memory_update_markdown.is_some() || request.history_entry.is_some()
{
SyncTurnStatus::Persisted
} else {
SyncTurnStatus::Noop
},
})
}
async fn on_session_end(
&self,
request: SessionEndRequest,
) -> crate::Result<SessionEndResponse> {
Ok(SessionEndResponse {
status: if request.session_id.is_some() {
SessionEndStatus::Triggered
} else {
SessionEndStatus::Noop
},
})
}
}
#[tokio::test]
async fn test_memory_provider_contract_is_domain_only() {
let provider = DummyProvider;
let prompt = provider
.system_prompt_block(&SystemPromptRequest {
workspace_root: PathBuf::from("/tmp/diva"),
})
.unwrap()
.prompt_block
.expect("dummy provider should return a prompt block");
assert!(prompt.markdown.contains("workspace=/tmp/diva"));
assert_eq!(prompt.shape, StartupInjectionShape::CompactRenderedMarkdown);
let prefetch = provider
.prefetch(PrefetchRequest {
workspace_root: PathBuf::from("/tmp/diva"),
intent: "recall-project-status".to_string(),
current_room: Some("roadmap".to_string()),
user_message: Some("what changed?".to_string()),
})
.await
.unwrap();
assert_eq!(prefetch.status, PrefetchStatus::Ready);
assert_eq!(prefetch.prompt_block.as_deref(), Some("intent=recall-project-status room=roadmap"));
let sync = provider
.sync_turn(SyncTurnRequest {
workspace_root: PathBuf::from("/tmp/diva"),
memory_update_markdown: Some("updated".to_string()),
history_entry: None,
})
.await
.unwrap();
assert_eq!(sync.status, SyncTurnStatus::Persisted);
let shutdown = provider
.on_session_end(SessionEndRequest {
workspace_root: PathBuf::from("/tmp/diva"),
session_id: Some("session-42".to_string()),
})
.await
.unwrap();
assert_eq!(shutdown.status, SessionEndStatus::Triggered);
}
#[test]
fn test_startup_context_snapshot_renders_compact_markdown() {
let block = StartupContextSnapshot {
laputa_state_root: Some(PathBuf::from("/tmp/diva/.laputa")),
soul_markdown: Some("# Identity\n\nGenerated soul".to_string()),
wakeup_markdown: None,
wakeup_pack: Some(WakeupPackSummary {
identity: "You are Diva.".to_string(),
recent_state: "- roadmap: Hot (heat: 5)".to_string(),
latest_capsule: Some("Weekly review complete.".to_string()),
key_relations: vec!["maintainer <-> roadmap".to_string()],
unresolved_threads: vec!["ship provider boundary".to_string()],
generated_at: Some("2026-05-08 10:00 UTC".to_string()),
}),
rhythm_triggers: vec![RhythmTrigger {
name: "weekly".to_string(),
reason: Some("capsule due".to_string()),
}],
memory_markdown: Some("## Long-term Memory\nExisting durable memory".to_string()),
}
.into_system_prompt_block()
.expect("startup context should render a prompt block");
assert_eq!(block.shape, StartupInjectionShape::CompactRenderedMarkdown);
assert!(block.markdown.contains("## Long-term Memory"));
assert!(block.markdown.contains("## Soul Projection"));
assert!(block.markdown.contains("## Wakeup Summary"));
assert!(block.markdown.contains("## Rhythm Signals"));
assert!(block.markdown.contains("weekly — capsule due"));
}
#[test]
fn test_degraded_startup_explicitly_omits_cached_wakeup() {
let response = SystemPromptResponse::degraded("wakeup generation failed");
match response.status {
StartupStatus::Degraded {
reason,
last_usable_wakeup,
} => {
assert_eq!(reason, "wakeup generation failed");
assert!(last_usable_wakeup.is_none());
}
other => panic!("expected degraded startup, got {other:?}"),
}
let block = response
.prompt_block
.expect("degraded startup should still emit an explicit prompt block");
assert!(block.markdown.contains("status: degraded"));
assert!(block.markdown.contains("last_usable_wakeup: omitted"));
}
}