Serialized AgentCycleState for Claude Code hook path
SavedAgentState (JSON) persists agent pid/phase/log_path across hook invocations. The Claude Code hook loads saved state, runs cycles, saves back. Pids are liveness-checked with kill(pid, 0) on load. No more scan_pid_files for agent lifecycle tracking. poc-agent keeps everything in memory (child handles). The hook path uses serialized state. Same AgentCycleState, different persistence model. Co-Authored-By: Proof of Concept <poc@bcachefs.org>
This commit is contained in:
parent
90d2717423
commit
fbc8572840
1 changed files with 78 additions and 11 deletions
|
|
@ -143,10 +143,10 @@ pub struct AgentInfo {
|
||||||
child: Option<std::process::Child>,
|
child: Option<std::process::Child>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Snapshot of agent state for sending to TUI (no Child handle).
|
/// Snapshot of agent state — serializable, sendable to TUI.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug, serde::Serialize, serde::Deserialize)]
|
||||||
pub struct AgentSnapshot {
|
pub struct AgentSnapshot {
|
||||||
pub name: &'static str,
|
pub name: String,
|
||||||
pub pid: Option<u32>,
|
pub pid: Option<u32>,
|
||||||
pub phase: Option<String>,
|
pub phase: Option<String>,
|
||||||
pub log_path: Option<std::path::PathBuf>,
|
pub log_path: Option<std::path::PathBuf>,
|
||||||
|
|
@ -155,7 +155,7 @@ pub struct AgentSnapshot {
|
||||||
impl AgentInfo {
|
impl AgentInfo {
|
||||||
fn snapshot(&self) -> AgentSnapshot {
|
fn snapshot(&self) -> AgentSnapshot {
|
||||||
AgentSnapshot {
|
AgentSnapshot {
|
||||||
name: self.name,
|
name: self.name.to_string(),
|
||||||
pid: self.pid,
|
pid: self.pid,
|
||||||
phase: self.phase.clone(),
|
phase: self.phase.clone(),
|
||||||
log_path: self.log_path.clone(),
|
log_path: self.log_path.clone(),
|
||||||
|
|
@ -163,6 +163,47 @@ impl AgentInfo {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Serializable state for persisting across Claude Code hook invocations.
|
||||||
|
#[derive(serde::Serialize, serde::Deserialize)]
|
||||||
|
pub struct SavedAgentState {
|
||||||
|
pub agents: Vec<AgentSnapshot>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SavedAgentState {
|
||||||
|
fn state_path(session_id: &str) -> std::path::PathBuf {
|
||||||
|
let dir = dirs::home_dir().unwrap_or_default().join(".consciousness/sessions");
|
||||||
|
fs::create_dir_all(&dir).ok();
|
||||||
|
dir.join(format!("agent-state-{}.json", session_id))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load(session_id: &str) -> Self {
|
||||||
|
let path = Self::state_path(session_id);
|
||||||
|
let mut state: Self = fs::read_to_string(&path).ok()
|
||||||
|
.and_then(|s| serde_json::from_str(&s).ok())
|
||||||
|
.unwrap_or(SavedAgentState { agents: Vec::new() });
|
||||||
|
|
||||||
|
// Check if saved pids are still alive
|
||||||
|
for agent in &mut state.agents {
|
||||||
|
if let Some(pid) = agent.pid {
|
||||||
|
unsafe {
|
||||||
|
if libc::kill(pid as i32, 0) != 0 {
|
||||||
|
agent.pid = None;
|
||||||
|
agent.phase = None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
state
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn save(&self, session_id: &str) {
|
||||||
|
let path = Self::state_path(session_id);
|
||||||
|
if let Ok(json) = serde_json::to_string(self) {
|
||||||
|
fs::write(path, json).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Persistent state for the agent orchestration cycle.
|
/// Persistent state for the agent orchestration cycle.
|
||||||
/// Created once, `trigger()` called on each user message.
|
/// Created once, `trigger()` called on each user message.
|
||||||
/// TUI reads `agents` and `last_output` for display.
|
/// TUI reads `agents` and `last_output` for display.
|
||||||
|
|
@ -207,7 +248,7 @@ impl AgentCycleState {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn agent_running(&self, name: &str) -> bool {
|
fn agent_running(&self, name: &str) -> bool {
|
||||||
self.agents.iter().any(|a| a.name == name && a.child.is_some())
|
self.agents.iter().any(|a| a.name == name && a.pid.is_some())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn agent_spawned(&mut self, name: &str, phase: &str,
|
fn agent_spawned(&mut self, name: &str, phase: &str,
|
||||||
|
|
@ -220,18 +261,23 @@ impl AgentCycleState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if any spawned agents have completed. Reap them.
|
/// Check if any agents have completed. Reap child handles, or
|
||||||
|
/// check pid liveness for restored-from-disk agents.
|
||||||
fn poll_children(&mut self) {
|
fn poll_children(&mut self) {
|
||||||
for agent in &mut self.agents {
|
for agent in &mut self.agents {
|
||||||
if let Some(ref mut child) = agent.child {
|
if let Some(ref mut child) = agent.child {
|
||||||
match child.try_wait() {
|
if let Ok(Some(_)) = child.try_wait() {
|
||||||
Ok(Some(_status)) => {
|
|
||||||
agent.pid = None;
|
agent.pid = None;
|
||||||
agent.phase = None;
|
agent.phase = None;
|
||||||
agent.child = None;
|
agent.child = None;
|
||||||
// log_path stays — TUI can still view the log
|
|
||||||
}
|
}
|
||||||
_ => {}
|
} else if let Some(pid) = agent.pid {
|
||||||
|
// No child handle (restored from saved state) — check pid
|
||||||
|
unsafe {
|
||||||
|
if libc::kill(pid as i32, 0) != 0 {
|
||||||
|
agent.pid = None;
|
||||||
|
agent.phase = None;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -241,6 +287,24 @@ impl AgentCycleState {
|
||||||
self.agents.iter().map(|a| a.snapshot()).collect()
|
self.agents.iter().map(|a| a.snapshot()).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Restore agent state from a saved snapshot (for Claude Code hook path).
|
||||||
|
pub fn restore(&mut self, saved: &SavedAgentState) {
|
||||||
|
for sa in &saved.agents {
|
||||||
|
if let Some(agent) = self.agents.iter_mut().find(|a| a.name == sa.name) {
|
||||||
|
agent.pid = sa.pid;
|
||||||
|
agent.phase = sa.phase.clone();
|
||||||
|
agent.log_path = sa.log_path.clone();
|
||||||
|
// No child handle — we just track the pid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Save current state for the Claude Code hook path.
|
||||||
|
pub fn save(&self, session_id: &str) {
|
||||||
|
let state = SavedAgentState { agents: self.snapshots() };
|
||||||
|
state.save(session_id);
|
||||||
|
}
|
||||||
|
|
||||||
/// Run all agent cycles. Call on each user message.
|
/// Run all agent cycles. Call on each user message.
|
||||||
pub fn trigger(&mut self, session: &HookSession) {
|
pub fn trigger(&mut self, session: &HookSession) {
|
||||||
let ts = chrono::Local::now().format("%Y-%m-%dT%H:%M:%S");
|
let ts = chrono::Local::now().format("%Y-%m-%dT%H:%M:%S");
|
||||||
|
|
@ -258,9 +322,12 @@ impl AgentCycleState {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Standalone entry point for the Claude Code hook path.
|
/// Standalone entry point for the Claude Code hook path.
|
||||||
|
/// Loads saved state, runs cycles, saves state back.
|
||||||
pub fn run_agent_cycles(session: &HookSession) -> AgentCycleOutput {
|
pub fn run_agent_cycles(session: &HookSession) -> AgentCycleOutput {
|
||||||
let mut state = AgentCycleState::new(&session.session_id);
|
let mut state = AgentCycleState::new(&session.session_id);
|
||||||
|
state.restore(&SavedAgentState::load(&session.session_id));
|
||||||
state.trigger(session);
|
state.trigger(session);
|
||||||
|
state.save(&session.session_id);
|
||||||
state.last_output
|
state.last_output
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue