Wire AgentCycleState through runner and TUI
Runner owns AgentCycleState, calls trigger() on each user message instead of the old run_hook() JSON round-trip. Sends AgentUpdate messages to TUI after each cycle. TUI F2 screen reads agent state from messages instead of scanning the filesystem on every frame. HookSession::from_fields() lets poc-agent construct sessions without JSON serialization. Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
parent
d097c8e067
commit
1c190a3925
5 changed files with 48 additions and 38 deletions
|
|
@ -74,6 +74,8 @@ pub struct Agent {
|
|||
pub shared_context: SharedContextState,
|
||||
/// Stable session ID for memory-search dedup across turns.
|
||||
session_id: String,
|
||||
/// Agent orchestration state (surface-observe, journal, reflect).
|
||||
pub agent_cycles: crate::subconscious::hook::AgentCycleState,
|
||||
}
|
||||
|
||||
impl Agent {
|
||||
|
|
@ -96,6 +98,7 @@ impl Agent {
|
|||
loaded_nodes: Vec::new(),
|
||||
};
|
||||
let session_id = format!("poc-agent-{}", chrono::Utc::now().format("%Y%m%d-%H%M%S"));
|
||||
let agent_cycles = crate::subconscious::hook::AgentCycleState::new(&session_id);
|
||||
let mut agent = Self {
|
||||
client,
|
||||
messages: Vec::new(),
|
||||
|
|
@ -109,6 +112,7 @@ impl Agent {
|
|||
context,
|
||||
shared_context,
|
||||
session_id,
|
||||
agent_cycles,
|
||||
};
|
||||
|
||||
// Load recent journal entries at startup for orientation
|
||||
|
|
@ -128,20 +132,20 @@ impl Agent {
|
|||
agent
|
||||
}
|
||||
|
||||
/// Run memory search for a given event, returning any output to inject.
|
||||
/// Direct library call — no subprocess needed since everything is one crate.
|
||||
fn run_hook(&self, event: &str, _prompt: &str) -> Option<String> {
|
||||
/// Run agent orchestration cycle and return formatted output to inject.
|
||||
fn run_agent_cycle(&mut self) -> Option<String> {
|
||||
let transcript_path = self.conversation_log.as_ref()
|
||||
.map(|l| l.path().to_string_lossy().to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
let hook_input = serde_json::json!({
|
||||
"hook_event_name": event,
|
||||
"session_id": self.session_id,
|
||||
"transcript_path": transcript_path,
|
||||
});
|
||||
let session = crate::session::HookSession::from_fields(
|
||||
self.session_id.clone(),
|
||||
transcript_path,
|
||||
"UserPromptSubmit".into(),
|
||||
);
|
||||
|
||||
let text = crate::memory_search::run_hook(&hook_input.to_string());
|
||||
self.agent_cycles.trigger(&session);
|
||||
let text = crate::subconscious::hook::format_agent_output(&self.agent_cycles.last_output);
|
||||
if text.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
|
|
@ -231,14 +235,15 @@ impl Agent {
|
|||
ui_tx: &UiSender,
|
||||
target: StreamTarget,
|
||||
) -> Result<TurnResult> {
|
||||
// Run poc-hook (memory search, notifications, context check)
|
||||
if let Some(hook_output) = self.run_hook("UserPromptSubmit", user_input) {
|
||||
// Run agent orchestration cycle (surface-observe, reflect, journal)
|
||||
if let Some(hook_output) = self.run_agent_cycle() {
|
||||
let enriched = format!("{}\n\n<system-reminder>\n{}\n</system-reminder>",
|
||||
user_input, hook_output);
|
||||
self.push_message(Message::user(enriched));
|
||||
} else {
|
||||
self.push_message(Message::user(user_input));
|
||||
}
|
||||
let _ = ui_tx.send(UiMessage::AgentUpdate(self.agent_cycles.agents.clone()));
|
||||
|
||||
let mut overflow_retries: u32 = 0;
|
||||
let mut empty_retries: u32 = 0;
|
||||
|
|
|
|||
|
|
@ -348,6 +348,8 @@ pub struct App {
|
|||
agent_selected: usize,
|
||||
/// Agent screen: viewing log for selected agent.
|
||||
agent_log_view: bool,
|
||||
/// Agent state from last cycle update.
|
||||
agent_state: Vec<crate::subconscious::hook::AgentInfo>,
|
||||
}
|
||||
|
||||
/// Overlay screens toggled by F-keys.
|
||||
|
|
@ -410,6 +412,7 @@ impl App {
|
|||
shared_context,
|
||||
agent_selected: 0,
|
||||
agent_log_view: false,
|
||||
agent_state: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -508,6 +511,9 @@ impl App {
|
|||
UiMessage::ContextInfoUpdate(info) => {
|
||||
self.context_info = Some(info);
|
||||
}
|
||||
UiMessage::AgentUpdate(agents) => {
|
||||
self.agent_state = agents;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1047,43 +1053,29 @@ impl App {
|
|||
lines.push(Line::raw(""));
|
||||
|
||||
for (i, &name) in AGENT_NAMES.iter().enumerate() {
|
||||
let agent_dir = output_dir.join(name);
|
||||
let live = crate::subconscious::knowledge::scan_pid_files(&agent_dir, 0);
|
||||
let selected = i == self.agent_selected;
|
||||
|
||||
let prefix = if selected { "▸ " } else { " " };
|
||||
let bg = if selected { Style::default().bg(Color::DarkGray) } else { Style::default() };
|
||||
|
||||
if live.is_empty() {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(format!("{}{:<20}", prefix, name), bg.fg(Color::Gray)),
|
||||
Span::styled("○ idle", bg.fg(Color::DarkGray)),
|
||||
]));
|
||||
} else {
|
||||
for (phase, pid) in &live {
|
||||
let agent = self.agent_state.iter().find(|a| a.name == name);
|
||||
|
||||
match agent.and_then(|a| a.pid) {
|
||||
Some(pid) => {
|
||||
let phase = agent.and_then(|a| a.phase.as_deref()).unwrap_or("?");
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(format!("{}{:<20}", prefix, name), bg.fg(Color::Green)),
|
||||
Span::styled("● ", bg.fg(Color::Green)),
|
||||
Span::styled(format!("pid {} phase: {}", pid, phase), bg),
|
||||
]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Recent output
|
||||
lines.push(Line::raw(""));
|
||||
lines.push(Line::styled("── Recent Activity ──", section));
|
||||
lines.push(Line::raw(""));
|
||||
|
||||
for &name in AGENT_NAMES {
|
||||
let agent_dir = output_dir.join(name);
|
||||
if let Some((file, ago)) = Self::most_recent_file(&agent_dir) {
|
||||
None => {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(format!(" {:<20}", name), dim),
|
||||
Span::raw(format!("{} ({})", file, ago)),
|
||||
Span::styled(format!("{}{:<20}", prefix, name), bg.fg(Color::Gray)),
|
||||
Span::styled("○ idle", bg.fg(Color::DarkGray)),
|
||||
]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let block = Block::default()
|
||||
.title_top(Line::from(SCREEN_LEGEND).left_aligned())
|
||||
|
|
|
|||
|
|
@ -124,6 +124,9 @@ pub enum UiMessage {
|
|||
|
||||
/// Context loading details — stored for the debug screen (Ctrl+D).
|
||||
ContextInfoUpdate(ContextInfo),
|
||||
|
||||
/// Agent cycle state update — refreshes the F2 agents screen.
|
||||
AgentUpdate(Vec<crate::subconscious::hook::AgentInfo>),
|
||||
}
|
||||
|
||||
/// Sender that fans out to both the TUI (mpsc) and observers (broadcast).
|
||||
|
|
|
|||
|
|
@ -40,6 +40,16 @@ impl HookSession {
|
|||
self.state_dir.join(format!("{}-{}", prefix, self.session_id))
|
||||
}
|
||||
|
||||
/// Construct directly from fields.
|
||||
pub fn from_fields(session_id: String, transcript_path: String, hook_event: String) -> Self {
|
||||
HookSession {
|
||||
state_dir: Self::sessions_dir(),
|
||||
session_id,
|
||||
transcript_path,
|
||||
hook_event,
|
||||
}
|
||||
}
|
||||
|
||||
/// Load from a session ID string
|
||||
pub fn from_id(session_id: String) -> Option<Self> {
|
||||
if session_id.is_empty() { return None; }
|
||||
|
|
|
|||
|
|
@ -135,11 +135,11 @@ pub struct AgentCycleOutput {
|
|||
}
|
||||
|
||||
/// Per-agent runtime state visible to the TUI.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct AgentInfo {
|
||||
pub name: &'static str,
|
||||
pub pid: Option<u32>,
|
||||
pub phase: Option<String>,
|
||||
pub last_log: Option<std::path::PathBuf>,
|
||||
}
|
||||
|
||||
/// Persistent state for the agent orchestration cycle.
|
||||
|
|
@ -164,7 +164,7 @@ impl AgentCycleState {
|
|||
.create(true).append(true).open(log_path).ok();
|
||||
|
||||
let agents = AGENT_CYCLE_NAMES.iter()
|
||||
.map(|&name| AgentInfo { name, pid: None, phase: None, last_log: None })
|
||||
.map(|&name| AgentInfo { name, pid: None, phase: None })
|
||||
.collect();
|
||||
|
||||
AgentCycleState {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue