Watch
1
0
Fork
You've already forked souveraine
0

alpha-readiness: Agent Health screen, $MEMORY_DIR fix, tool posture badge

- Settings Agent category now shows agent ID, subconscious pairing,
  memory/subconscious paths, and disk-existence checks (read-only)
- $MEMORY_DIR env var now always set correctly — strips stale host-shell
  leftovers before injecting agent-specific computed values
- Header bar shows [tools shown] / [tools folded] posture badge
- Fix "applies live" footer — only shown for categories with live fields
- Bifrost "primary model" relabeled to "new-agent default" for clarity
- Update alpha-tester-readiness.md task doc and INDEX.md task tracker
This commit is contained in:
Fimeg 2026-05-20 11:42:22 -04:00
commit 74887d32a3
6 changed files with 116 additions and 36 deletions

View file

@ -113,8 +113,9 @@ impl ToolContext {
/// - `AGENT_ID` / `LETTA_AGENT_ID` / `SOUVERAINE_AGENT_ID` — her own
/// identifier so skills that scope by agent can resolve.
///
/// Any value already present in `env` is left alone — the caller's
/// override wins.
/// Memory and agent-id vars are always set (overriding any stale values
/// inherited from the host shell — e.g. a leftover `$MEMORY_DIR` from
/// the Letta era). Other env keys from the caller are preserved.
pub fn for_agent(
agent_id: impl Into<String>,
cwd: Option<PathBuf>,
@ -124,18 +125,22 @@ impl ToolContext {
) -> Self {
let agent_id_str = agent_id.into();
// Strip stale values so the computed override always wins.
env.retain(|(k, _)| {
!matches!(k.as_str(),
"MEMORY_DIR" | "LETTA_MEMORY_DIR" | "SOUVERAINE_MEMORY_DIR" | "MEMORY"
| "AGENT_ID" | "LETTA_AGENT_ID" | "SOUVERAINE_AGENT_ID"
)
});
if let Some(root) = memory_root.as_ref() {
let root_str = root.display().to_string();
for key in ["MEMORY_DIR", "LETTA_MEMORY_DIR", "SOUVERAINE_MEMORY_DIR", "MEMORY"] {
if !env.iter().any(|(k, _)| k == key) {
env.push((key.to_string(), root_str.clone()));
}
env.push((key.to_string(), root_str.clone()));
}
}
for key in ["AGENT_ID", "LETTA_AGENT_ID", "SOUVERAINE_AGENT_ID"] {
if !env.iter().any(|(k, _)| k == key) {
env.push((key.to_string(), agent_id_str.clone()));
}
env.push((key.to_string(), agent_id_str.clone()));
}
Self {

View file

@ -233,11 +233,21 @@ impl App {
self.chat.as_ref()?; // a backend is required to persist the change
let id = self.agent_id_by_name(&self.agent_pref)?;
let model = Self::agent_model_from_disk(&id)?;
let base = dirs::home_dir()?;
let memory_root = base.join(".souveraine/agents").join(&id).join("memory");
let subconscious_root = base.join(".souveraine/subconscious-agents").join(format!("{id}-sub")).join("memory.git");
let has_subconscious = subconscious_root.join("HEAD").exists();
let agent_json_exists = base.join(".souveraine/server/agents").join(&id).join("agent.json").exists();
Some(crate::ui::settings::ActiveAgentSettings {
id,
id: id.clone(),
name: self.agent_pref.clone(),
model: model.clone(),
model_original: model,
subconscious_id: format!("{id}-sub"),
memory_root,
subconscious_root,
has_subconscious,
agent_json_exists,
})
}

View file

@ -152,10 +152,19 @@ fn draw_header(f: &mut Frame, state: &ChatState, area: Rect) {
"remote" => state.palette.agent_primary,
_ => state.palette.agent_dim,
};
let posture_label = match state.render_mode {
super::ChatMode::Code => "tools shown",
super::ChatMode::Conversation => "tools folded",
};
let posture_color = match state.render_mode {
super::ChatMode::Code => state.palette.tool_accent,
super::ChatMode::Conversation => state.palette.agent_dim,
};
let title = Line::from(vec![
Span::styled("✦ Souveraine ", Style::default().fg(state.palette.agent_primary).add_modifier(Modifier::BOLD)),
Span::styled(format!("· {} ", state.agent_name), Style::default().fg(Color::White)),
Span::styled(format!("[{} mode]", state.mode), Style::default().fg(mode_color)),
Span::styled(format!("[{}] ", state.mode), Style::default().fg(mode_color)),
Span::styled(format!("[{}]", posture_label), Style::default().fg(posture_color)),
]);
f.render_widget(Paragraph::new(title).alignment(Alignment::Center), area);
}

View file

@ -90,8 +90,11 @@ fn draw_field_panel(frame: &mut Frame, area: Rect, view: &SettingsView) {
lines.push(Line::from(""));
let p = &view.palette;
let has_applies_live = fields.iter().any(|(loc, _)| loc.applies_live());
for (i, (loc, value)) in fields.iter().enumerate() {
let is_selected = i == view.field_idx && view.focus == PanelFocus::Fields;
let is_readonly = loc.is_readonly();
let is_selected = i == view.field_idx && view.focus == PanelFocus::Fields && !is_readonly;
let key_style = if is_selected {
Style::default().fg(p.agent_primary).add_modifier(Modifier::BOLD)
} else {
@ -103,24 +106,26 @@ fn draw_field_panel(frame: &mut Frame, area: Rect, view: &SettingsView) {
];
// If this field is being edited, show the buffer with cursor.
if let SettingsMode::Editing { loc: edit_loc, buffer, cursor } = &view.mode {
if *edit_loc == *loc {
let before = &buffer[..*cursor];
let after = &buffer[*cursor..];
spans.push(Span::styled(
before.to_string(),
Style::default().fg(p.agent_primary).add_modifier(Modifier::BOLD),
));
spans.push(Span::styled(
'▏'.to_string(),
Style::default().fg(Color::White).add_modifier(Modifier::SLOW_BLINK),
));
spans.push(Span::styled(
after.to_string(),
Style::default().fg(p.agent_primary).add_modifier(Modifier::BOLD),
));
lines.push(Line::from(spans));
continue;
if !is_readonly {
if let SettingsMode::Editing { loc: edit_loc, buffer, cursor } = &view.mode {
if *edit_loc == *loc {
let before = &buffer[..*cursor];
let after = &buffer[*cursor..];
spans.push(Span::styled(
before.to_string(),
Style::default().fg(p.agent_primary).add_modifier(Modifier::BOLD),
));
spans.push(Span::styled(
'▏'.to_string(),
Style::default().fg(Color::White).add_modifier(Modifier::SLOW_BLINK),
));
spans.push(Span::styled(
after.to_string(),
Style::default().fg(p.agent_primary).add_modifier(Modifier::BOLD),
));
lines.push(Line::from(spans));
continue;
}
}
}
@ -131,11 +136,13 @@ fn draw_field_panel(frame: &mut Frame, area: Rect, view: &SettingsView) {
lines.push(Line::from(spans));
}
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(
" ◆ applies live · other changes take effect on restart",
Style::default().fg(p.agent_dim).add_modifier(Modifier::ITALIC),
)));
if has_applies_live {
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(
" ◆ applies live · other changes take effect on restart",
Style::default().fg(p.agent_dim).add_modifier(Modifier::ITALIC),
)));
}
let focused = view.focus == PanelFocus::Fields;
let border_color = if view.dirty {

View file

@ -173,6 +173,12 @@ pub enum FieldLoc {
// TUI
TuShowInterstitial,
TuCennoThreshold,
// Agent (read-only diagnostics)
AgAgentId,
AgSubconsciousId,
AgMemoryPath,
AgSubconsciousPath,
AgSubconsciousStatus,
}
impl FieldLoc {
@ -198,6 +204,8 @@ impl FieldLoc {
FieldLoc::PrPulseEnabled | FieldLoc::PrPulseIntervalSecs | FieldLoc::PrOutfit | FieldLoc::PrAtmosphere => Category::Presence,
FieldLoc::VcEnabled | FieldLoc::VcSttUrl | FieldLoc::VcTtsUrl | FieldLoc::VcVoiceId | FieldLoc::VcPushToTalkKey => Category::Voice,
FieldLoc::TuShowInterstitial | FieldLoc::TuCennoThreshold => Category::Tui,
FieldLoc::AgAgentId | FieldLoc::AgSubconsciousId | FieldLoc::AgMemoryPath
| FieldLoc::AgSubconsciousPath | FieldLoc::AgSubconsciousStatus => Category::Agent,
}
}
@ -272,6 +280,11 @@ impl FieldLoc {
FieldLoc::FdAutoWake => "auto_wake",
FieldLoc::TuShowInterstitial => "show_interstitial",
FieldLoc::TuCennoThreshold => "cenno_word_threshold",
FieldLoc::AgAgentId => "agent_id",
FieldLoc::AgSubconsciousId => "subconscious_id",
FieldLoc::AgMemoryPath => "memory_path",
FieldLoc::AgSubconsciousPath => "subconscious_path",
FieldLoc::AgSubconsciousStatus => "subconscious_ok",
}
}
@ -280,6 +293,18 @@ impl FieldLoc {
matches!(self, FieldLoc::BfApiKey | FieldLoc::BfVirtualKey)
}
/// Returns true for informational fields that cannot be edited.
pub fn is_readonly(&self) -> bool {
matches!(
self,
FieldLoc::AgAgentId
| FieldLoc::AgSubconsciousId
| FieldLoc::AgMemoryPath
| FieldLoc::AgSubconsciousPath
| FieldLoc::AgSubconsciousStatus
)
}
pub fn label(&self) -> &'static str {
match self {
FieldLoc::AgSystemPrompt => "platform prompt",
@ -287,7 +312,7 @@ impl FieldLoc {
FieldLoc::BfBaseUrl => "endpoint",
FieldLoc::BfApiKey => "API key",
FieldLoc::BfVirtualKey => "virtual key",
FieldLoc::BfPrimaryModel => "primary model",
FieldLoc::BfPrimaryModel => "new-agent default",
FieldLoc::ScN1Enabled => "N+1 enabled",
FieldLoc::ScN1Trigger => "N+1 trigger",
FieldLoc::ScN1Every => " └ every N responses",
@ -351,6 +376,11 @@ impl FieldLoc {
FieldLoc::FdAutoWake => "auto-wake on summon",
FieldLoc::TuShowInterstitial => "interstitial narration",
FieldLoc::TuCennoThreshold => "cenno threshold (words)",
FieldLoc::AgAgentId => "agent id",
FieldLoc::AgSubconsciousId => "subconscious",
FieldLoc::AgMemoryPath => "memory path",
FieldLoc::AgSubconsciousPath => "subconscious path",
FieldLoc::AgSubconsciousStatus => "subconscious on disk",
}
}

View file

@ -1,4 +1,4 @@
use std::path::Path;
use std::path::{Path, PathBuf};
use tokio::sync::oneshot;
@ -24,6 +24,16 @@ pub struct ActiveAgentSettings {
pub model: String,
/// The model as loaded — for save-time diffing.
pub model_original: String,
/// Display-only: the paired subconscious agent id (always `{id}-sub`).
pub subconscious_id: String,
/// Display-only: the primary's memory directory path.
pub memory_root: PathBuf,
/// Display-only: the subconscious agent's memory directory path.
pub subconscious_root: PathBuf,
/// Display-only: whether the subconscious directory + memory.git exist.
pub has_subconscious: bool,
/// Display-only: whether the agent's own agent.json is readable.
pub agent_json_exists: bool,
}
pub struct SettingsView {
@ -154,6 +164,12 @@ impl SettingsView {
let idx = variants.iter().position(|m| m == &agent.model).unwrap_or(0);
out.push((AgModel, EditableValue::EnumVariant { index: idx, variants }));
}
// Read-only diagnostics
out.push((AgAgentId, EditableValue::Text(agent.id.clone())));
out.push((AgSubconsciousId, EditableValue::Text(agent.subconscious_id.clone())));
out.push((AgMemoryPath, EditableValue::Text(agent.memory_root.to_string_lossy().to_string())));
out.push((AgSubconsciousPath, EditableValue::Text(agent.subconscious_root.to_string_lossy().to_string())));
out.push((AgSubconsciousStatus, EditableValue::Bool(agent.has_subconscious)));
}
}
Category::Bifrost => {
@ -614,6 +630,9 @@ impl SettingsView {
TuShowInterstitial => { if let EditableValue::Bool(v) = value { self.config.tui.show_interstitial = v; } }
TuCennoThreshold => { if let EditableValue::Uint(v) = value { self.config.tui.cenno_word_threshold = v as usize; } }
// Read-only diagnostic fields — no-op on apply.
AgAgentId | AgSubconsciousId | AgMemoryPath | AgSubconsciousPath | AgSubconsciousStatus => {}
}
self.dirty = true;
}