Subconscious screen: detail view with post-fork entries
Track fork point in run_forked(), capture entries added during the run. Subconscious screen shows these in a detail view (Enter to drill in, Esc to go back) — only the subconscious agent's own conversation, not the inherited conscious context. Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
parent
c2a3844d69
commit
ef868cb98f
3 changed files with 127 additions and 6 deletions
|
|
@ -59,6 +59,9 @@ pub struct AutoAgent {
|
||||||
/// Named outputs from the agent's output() tool calls.
|
/// Named outputs from the agent's output() tool calls.
|
||||||
/// Collected per-run, read by Mind after completion.
|
/// Collected per-run, read by Mind after completion.
|
||||||
pub outputs: std::collections::HashMap<String, String>,
|
pub outputs: std::collections::HashMap<String, String>,
|
||||||
|
/// Entries added during the last forked run (after the fork point).
|
||||||
|
/// The subconscious screen shows these.
|
||||||
|
pub last_run_entries: Vec<super::context::ConversationEntry>,
|
||||||
// Observable status
|
// Observable status
|
||||||
pub current_phase: String,
|
pub current_phase: String,
|
||||||
pub turn: usize,
|
pub turn: usize,
|
||||||
|
|
@ -163,6 +166,7 @@ impl AutoAgent {
|
||||||
priority,
|
priority,
|
||||||
walked: Vec::new(),
|
walked: Vec::new(),
|
||||||
outputs: std::collections::HashMap::new(),
|
outputs: std::collections::HashMap::new(),
|
||||||
|
last_run_entries: Vec::new(),
|
||||||
current_phase: String::new(),
|
current_phase: String::new(),
|
||||||
turn: 0,
|
turn: 0,
|
||||||
}
|
}
|
||||||
|
|
@ -198,9 +202,14 @@ impl AutoAgent {
|
||||||
}).collect();
|
}).collect();
|
||||||
let orig_steps = std::mem::replace(&mut self.steps, resolved_steps);
|
let orig_steps = std::mem::replace(&mut self.steps, resolved_steps);
|
||||||
let forked = agent.fork(self.tools.clone());
|
let forked = agent.fork(self.tools.clone());
|
||||||
|
let fork_point = forked.context.entries.len();
|
||||||
let mut backend = Backend::Forked(forked);
|
let mut backend = Backend::Forked(forked);
|
||||||
let result = self.run_with_backend(&mut backend, None).await;
|
let result = self.run_with_backend(&mut backend, None).await;
|
||||||
self.steps = orig_steps; // restore templates
|
// Capture entries added during this run
|
||||||
|
if let Backend::Forked(ref agent) = backend {
|
||||||
|
self.last_run_entries = agent.context.entries[fork_point..].to_vec();
|
||||||
|
}
|
||||||
|
self.steps = orig_steps;
|
||||||
result
|
result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -100,6 +100,8 @@ pub struct SubconsciousSnapshot {
|
||||||
pub turn: usize,
|
pub turn: usize,
|
||||||
pub walked_count: usize,
|
pub walked_count: usize,
|
||||||
pub last_run_secs_ago: Option<f64>,
|
pub last_run_secs_ago: Option<f64>,
|
||||||
|
/// Entries from the last forked run (after fork point).
|
||||||
|
pub last_run_entries: Vec<crate::agent::context::ConversationEntry>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SubconsciousAgent {
|
impl SubconsciousAgent {
|
||||||
|
|
@ -111,6 +113,7 @@ impl SubconsciousAgent {
|
||||||
turn: self.auto.turn,
|
turn: self.auto.turn,
|
||||||
walked_count: self.auto.walked.len(),
|
walked_count: self.auto.walked.len(),
|
||||||
last_run_secs_ago: self.last_run.map(|t| t.elapsed().as_secs_f64()),
|
last_run_secs_ago: self.last_run.map(|t| t.elapsed().as_secs_f64()),
|
||||||
|
last_run_entries: self.auto.last_run_entries.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,15 +10,18 @@ use ratatui::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{App, ScreenView, screen_legend};
|
use super::{App, ScreenView, screen_legend};
|
||||||
|
use crate::agent::context::ConversationEntry;
|
||||||
|
use crate::agent::api::types::Role;
|
||||||
|
|
||||||
pub(crate) struct SubconsciousScreen {
|
pub(crate) struct SubconsciousScreen {
|
||||||
selected: usize,
|
selected: usize,
|
||||||
|
detail: bool,
|
||||||
scroll: u16,
|
scroll: u16,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SubconsciousScreen {
|
impl SubconsciousScreen {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self { selected: 0, scroll: 0 }
|
Self { selected: 0, detail: false, scroll: 0 }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -31,13 +34,26 @@ impl ScreenView for SubconsciousScreen {
|
||||||
if let ratatui::crossterm::event::Event::Key(key) = event {
|
if let ratatui::crossterm::event::Event::Key(key) = event {
|
||||||
if key.kind != ratatui::crossterm::event::KeyEventKind::Press { continue; }
|
if key.kind != ratatui::crossterm::event::KeyEventKind::Press { continue; }
|
||||||
match key.code {
|
match key.code {
|
||||||
KeyCode::Up => {
|
KeyCode::Up if !self.detail => {
|
||||||
self.selected = self.selected.saturating_sub(1);
|
self.selected = self.selected.saturating_sub(1);
|
||||||
}
|
}
|
||||||
KeyCode::Down => {
|
KeyCode::Down if !self.detail => {
|
||||||
self.selected = (self.selected + 1)
|
self.selected = (self.selected + 1)
|
||||||
.min(app.agent_state.len().saturating_sub(1));
|
.min(app.agent_state.len().saturating_sub(1));
|
||||||
}
|
}
|
||||||
|
KeyCode::Enter | KeyCode::Right if !self.detail => {
|
||||||
|
self.detail = true;
|
||||||
|
self.scroll = 0;
|
||||||
|
}
|
||||||
|
KeyCode::Esc | KeyCode::Left if self.detail => {
|
||||||
|
self.detail = false;
|
||||||
|
}
|
||||||
|
KeyCode::Up if self.detail => {
|
||||||
|
self.scroll = self.scroll.saturating_sub(3);
|
||||||
|
}
|
||||||
|
KeyCode::Down if self.detail => {
|
||||||
|
self.scroll += 3;
|
||||||
|
}
|
||||||
KeyCode::PageUp => { self.scroll = self.scroll.saturating_sub(20); }
|
KeyCode::PageUp => { self.scroll = self.scroll.saturating_sub(20); }
|
||||||
KeyCode::PageDown => { self.scroll += 20; }
|
KeyCode::PageDown => { self.scroll += 20; }
|
||||||
_ => {}
|
_ => {}
|
||||||
|
|
@ -45,13 +61,23 @@ impl ScreenView for SubconsciousScreen {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.detail {
|
||||||
|
self.draw_detail(frame, area, app);
|
||||||
|
} else {
|
||||||
|
self.draw_list(frame, area, app);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SubconsciousScreen {
|
||||||
|
fn draw_list(&self, frame: &mut Frame, area: Rect, app: &App) {
|
||||||
let mut lines: Vec<Line> = Vec::new();
|
let mut lines: Vec<Line> = Vec::new();
|
||||||
let section = Style::default().fg(Color::Yellow);
|
let section = Style::default().fg(Color::Yellow);
|
||||||
let hint = Style::default().fg(Color::DarkGray).add_modifier(Modifier::ITALIC);
|
let hint = Style::default().fg(Color::DarkGray).add_modifier(Modifier::ITALIC);
|
||||||
|
|
||||||
lines.push(Line::raw(""));
|
lines.push(Line::raw(""));
|
||||||
lines.push(Line::styled("── Subconscious Agents ──", section));
|
lines.push(Line::styled("── Subconscious Agents ──", section));
|
||||||
lines.push(Line::styled(" (↑/↓ select)", hint));
|
lines.push(Line::styled(" (↑/↓ select, Enter view log)", hint));
|
||||||
lines.push(Line::raw(""));
|
lines.push(Line::raw(""));
|
||||||
|
|
||||||
if app.agent_state.is_empty() {
|
if app.agent_state.is_empty() {
|
||||||
|
|
@ -87,6 +113,7 @@ impl ScreenView for SubconsciousScreen {
|
||||||
else { format!("{:.1}h ago", s / 3600.0) }
|
else { format!("{:.1}h ago", s / 3600.0) }
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| "never".to_string());
|
.unwrap_or_else(|| "never".to_string());
|
||||||
|
let entries = snap.last_run_entries.len();
|
||||||
vec![
|
vec![
|
||||||
Span::styled(
|
Span::styled(
|
||||||
format!("{}{:<30}", prefix, snap.name),
|
format!("{}{:<30}", prefix, snap.name),
|
||||||
|
|
@ -94,7 +121,8 @@ impl ScreenView for SubconsciousScreen {
|
||||||
),
|
),
|
||||||
Span::styled("○ ", bg.fg(Color::DarkGray)),
|
Span::styled("○ ", bg.fg(Color::DarkGray)),
|
||||||
Span::styled(
|
Span::styled(
|
||||||
format!("idle last: {} walked: {}", ago, snap.walked_count),
|
format!("idle last: {} entries: {} walked: {}",
|
||||||
|
ago, entries, snap.walked_count),
|
||||||
bg.fg(Color::DarkGray),
|
bg.fg(Color::DarkGray),
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
@ -114,4 +142,85 @@ impl ScreenView for SubconsciousScreen {
|
||||||
.scroll((self.scroll, 0));
|
.scroll((self.scroll, 0));
|
||||||
frame.render_widget(para, area);
|
frame.render_widget(para, area);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn draw_detail(&self, frame: &mut Frame, area: Rect, app: &App) {
|
||||||
|
let snap = match app.agent_state.get(self.selected) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => return,
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut lines: Vec<Line> = Vec::new();
|
||||||
|
let section = Style::default().fg(Color::Yellow);
|
||||||
|
let hint = Style::default().fg(Color::DarkGray).add_modifier(Modifier::ITALIC);
|
||||||
|
|
||||||
|
lines.push(Line::raw(""));
|
||||||
|
lines.push(Line::styled(format!("── {} ──", snap.name), section));
|
||||||
|
lines.push(Line::styled(" (Esc/← back, ↑/↓/PgUp/PgDn scroll)", hint));
|
||||||
|
lines.push(Line::raw(""));
|
||||||
|
|
||||||
|
if snap.last_run_entries.is_empty() {
|
||||||
|
lines.push(Line::styled(" (no run data)", hint));
|
||||||
|
}
|
||||||
|
|
||||||
|
for entry in &snap.last_run_entries {
|
||||||
|
if entry.is_log() {
|
||||||
|
if let ConversationEntry::Log(text) = entry {
|
||||||
|
lines.push(Line::styled(
|
||||||
|
format!(" [log] {}", text),
|
||||||
|
Style::default().fg(Color::DarkGray),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let msg = entry.message();
|
||||||
|
let (role_str, role_color) = match msg.role {
|
||||||
|
Role::User => ("user", Color::Cyan),
|
||||||
|
Role::Assistant => ("assistant", Color::Reset),
|
||||||
|
Role::Tool => ("tool", Color::DarkGray),
|
||||||
|
Role::System => ("system", Color::Yellow),
|
||||||
|
};
|
||||||
|
|
||||||
|
let text = msg.content_text();
|
||||||
|
let tool_info = msg.tool_calls.as_ref().map(|tc| {
|
||||||
|
tc.iter().map(|c| c.function.name.as_str())
|
||||||
|
.collect::<Vec<_>>().join(", ")
|
||||||
|
});
|
||||||
|
|
||||||
|
// Role header
|
||||||
|
let header = match &tool_info {
|
||||||
|
Some(tools) => format!(" [{} → {}]", role_str, tools),
|
||||||
|
None => format!(" [{}]", role_str),
|
||||||
|
};
|
||||||
|
lines.push(Line::styled(header, Style::default().fg(role_color)));
|
||||||
|
|
||||||
|
// Content (truncated per line)
|
||||||
|
if !text.is_empty() {
|
||||||
|
for line in text.lines().take(20) {
|
||||||
|
lines.push(Line::styled(
|
||||||
|
format!(" {}", line),
|
||||||
|
Style::default().fg(Color::Gray),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if text.lines().count() > 20 {
|
||||||
|
lines.push(Line::styled(
|
||||||
|
format!(" ... ({} more lines)", text.lines().count() - 20),
|
||||||
|
hint,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let block = Block::default()
|
||||||
|
.title_top(Line::from(screen_legend()).left_aligned())
|
||||||
|
.title_top(Line::from(format!(" {} ", snap.name)).right_aligned())
|
||||||
|
.borders(Borders::ALL)
|
||||||
|
.border_style(Style::default().fg(Color::Cyan));
|
||||||
|
|
||||||
|
let para = Paragraph::new(lines)
|
||||||
|
.block(block)
|
||||||
|
.wrap(Wrap { trim: false })
|
||||||
|
.scroll((self.scroll, 0));
|
||||||
|
frame.render_widget(para, area);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue