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:
Kent Overstreet 2026-04-02 00:52:57 -04:00
parent d097c8e067
commit 1c190a3925
5 changed files with 48 additions and 38 deletions

View file

@ -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;