story-kit: accept 96_story_reset_agent_lozenge_to_idle_state_when_returning_to_roster
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
use crate::agent_log::AgentLogWriter;
|
||||
use crate::config::ProjectConfig;
|
||||
use crate::worktree::{self, WorktreeInfo};
|
||||
use portable_pty::{CommandBuilder, PtySize, native_pty_system};
|
||||
@@ -113,6 +114,8 @@ pub struct AgentInfo {
|
||||
pub worktree_path: Option<String>,
|
||||
pub base_branch: Option<String>,
|
||||
pub completion: Option<CompletionReport>,
|
||||
/// UUID identifying the persistent log file for this session.
|
||||
pub log_session_id: Option<String>,
|
||||
}
|
||||
|
||||
struct StoryAgent {
|
||||
@@ -128,6 +131,8 @@ struct StoryAgent {
|
||||
completion: Option<CompletionReport>,
|
||||
/// Project root, stored for pipeline advancement after completion.
|
||||
project_root: Option<PathBuf>,
|
||||
/// UUID identifying the log file for this session.
|
||||
log_session_id: Option<String>,
|
||||
}
|
||||
|
||||
/// Build an `AgentInfo` snapshot from a `StoryAgent` map entry.
|
||||
@@ -146,6 +151,7 @@ fn agent_info_from_entry(story_id: &str, agent: &StoryAgent) -> AgentInfo {
|
||||
.as_ref()
|
||||
.map(|wt| wt.base_branch.clone()),
|
||||
completion: agent.completion.clone(),
|
||||
log_session_id: agent.log_session_id.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,6 +216,23 @@ impl AgentPool {
|
||||
|
||||
let event_log: Arc<Mutex<Vec<AgentEvent>>> = Arc::new(Mutex::new(Vec::new()));
|
||||
|
||||
// Generate a unique session ID for the persistent log file.
|
||||
let log_session_id = uuid::Uuid::new_v4().to_string();
|
||||
|
||||
// Create persistent log writer.
|
||||
let log_writer = match AgentLogWriter::new(
|
||||
project_root,
|
||||
story_id,
|
||||
&resolved_name,
|
||||
&log_session_id,
|
||||
) {
|
||||
Ok(w) => Some(Arc::new(Mutex::new(w))),
|
||||
Err(e) => {
|
||||
eprintln!("[agents] Failed to create log writer for {story_id}:{resolved_name}: {e}");
|
||||
None
|
||||
}
|
||||
};
|
||||
|
||||
// Register as pending
|
||||
{
|
||||
let mut agents = self.agents.lock().map_err(|e| e.to_string())?;
|
||||
@@ -225,6 +248,7 @@ impl AgentPool {
|
||||
event_log: event_log.clone(),
|
||||
completion: None,
|
||||
project_root: Some(project_root.to_path_buf()),
|
||||
log_session_id: Some(log_session_id.clone()),
|
||||
},
|
||||
);
|
||||
}
|
||||
@@ -267,6 +291,7 @@ impl AgentPool {
|
||||
let key_clone = key.clone();
|
||||
let log_clone = event_log.clone();
|
||||
let port_for_task = self.port;
|
||||
let log_writer_clone = log_writer.clone();
|
||||
|
||||
let handle = tokio::spawn(async move {
|
||||
let _ = tx_clone.send(AgentEvent::Status {
|
||||
@@ -277,6 +302,7 @@ impl AgentPool {
|
||||
|
||||
match run_agent_pty_streaming(
|
||||
&sid, &aname, &command, &args, &prompt, &cwd, &tx_clone, &log_clone,
|
||||
log_writer_clone,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -324,6 +350,7 @@ impl AgentPool {
|
||||
worktree_path: Some(wt_path_str),
|
||||
base_branch: Some(wt_info.base_branch.clone()),
|
||||
completion: None,
|
||||
log_session_id: Some(log_session_id),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -487,6 +514,7 @@ impl AgentPool {
|
||||
worktree_path: None,
|
||||
base_branch: None,
|
||||
completion: None,
|
||||
log_session_id: None,
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -962,6 +990,22 @@ impl AgentPool {
|
||||
state.get_project_root()
|
||||
}
|
||||
|
||||
/// Get the log session ID and project root for an agent, if available.
|
||||
///
|
||||
/// Used by MCP tools to find the persistent log file for a completed agent.
|
||||
pub fn get_log_info(
|
||||
&self,
|
||||
story_id: &str,
|
||||
agent_name: &str,
|
||||
) -> Option<(String, PathBuf)> {
|
||||
let key = composite_key(story_id, agent_name);
|
||||
let agents = self.agents.lock().ok()?;
|
||||
let agent = agents.get(&key)?;
|
||||
let session_id = agent.log_session_id.clone()?;
|
||||
let project_root = agent.project_root.clone()?;
|
||||
Some((session_id, project_root))
|
||||
}
|
||||
|
||||
/// Test helper: inject a pre-built agent entry so unit tests can exercise
|
||||
/// wait/subscribe logic without spawning a real process.
|
||||
#[cfg(test)]
|
||||
@@ -986,6 +1030,7 @@ impl AgentPool {
|
||||
event_log: Arc::new(Mutex::new(Vec::new())),
|
||||
completion: None,
|
||||
project_root: None,
|
||||
log_session_id: None,
|
||||
},
|
||||
);
|
||||
tx
|
||||
@@ -1020,6 +1065,7 @@ impl AgentPool {
|
||||
event_log: Arc::new(Mutex::new(Vec::new())),
|
||||
completion: None,
|
||||
project_root: None,
|
||||
log_session_id: None,
|
||||
},
|
||||
);
|
||||
tx
|
||||
@@ -1279,6 +1325,7 @@ impl AgentPool {
|
||||
event_log: Arc::new(Mutex::new(Vec::new())),
|
||||
completion: Some(completion),
|
||||
project_root: Some(project_root),
|
||||
log_session_id: None,
|
||||
},
|
||||
);
|
||||
tx
|
||||
@@ -2078,6 +2125,7 @@ async fn run_agent_pty_streaming(
|
||||
cwd: &str,
|
||||
tx: &broadcast::Sender<AgentEvent>,
|
||||
event_log: &Arc<Mutex<Vec<AgentEvent>>>,
|
||||
log_writer: Option<Arc<Mutex<AgentLogWriter>>>,
|
||||
) -> Result<Option<String>, String> {
|
||||
let sid = story_id.to_string();
|
||||
let aname = agent_name.to_string();
|
||||
@@ -2089,21 +2137,38 @@ async fn run_agent_pty_streaming(
|
||||
let event_log = event_log.clone();
|
||||
|
||||
tokio::task::spawn_blocking(move || {
|
||||
run_agent_pty_blocking(&sid, &aname, &cmd, &args, &prompt, &cwd, &tx, &event_log)
|
||||
run_agent_pty_blocking(
|
||||
&sid,
|
||||
&aname,
|
||||
&cmd,
|
||||
&args,
|
||||
&prompt,
|
||||
&cwd,
|
||||
&tx,
|
||||
&event_log,
|
||||
log_writer.as_deref(),
|
||||
)
|
||||
})
|
||||
.await
|
||||
.map_err(|e| format!("Agent task panicked: {e}"))?
|
||||
}
|
||||
|
||||
/// Helper to send an event to both broadcast and event log.
|
||||
/// Helper to send an event to broadcast, event log, and optional persistent log file.
|
||||
fn emit_event(
|
||||
event: AgentEvent,
|
||||
tx: &broadcast::Sender<AgentEvent>,
|
||||
event_log: &Mutex<Vec<AgentEvent>>,
|
||||
log_writer: Option<&Mutex<AgentLogWriter>>,
|
||||
) {
|
||||
if let Ok(mut log) = event_log.lock() {
|
||||
log.push(event.clone());
|
||||
}
|
||||
if let Some(writer) = log_writer
|
||||
&& let Ok(mut w) = writer.lock()
|
||||
&& let Err(e) = w.write_event(&event)
|
||||
{
|
||||
eprintln!("[agent_log] Failed to write event to log file: {e}");
|
||||
}
|
||||
let _ = tx.send(event);
|
||||
}
|
||||
|
||||
@@ -2117,6 +2182,7 @@ fn run_agent_pty_blocking(
|
||||
cwd: &str,
|
||||
tx: &broadcast::Sender<AgentEvent>,
|
||||
event_log: &Mutex<Vec<AgentEvent>>,
|
||||
log_writer: Option<&Mutex<AgentLogWriter>>,
|
||||
) -> Result<Option<String>, String> {
|
||||
let pty_system = native_pty_system();
|
||||
|
||||
@@ -2198,6 +2264,7 @@ fn run_agent_pty_blocking(
|
||||
},
|
||||
tx,
|
||||
event_log,
|
||||
log_writer,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
@@ -2226,6 +2293,7 @@ fn run_agent_pty_blocking(
|
||||
},
|
||||
tx,
|
||||
event_log,
|
||||
log_writer,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2243,6 +2311,7 @@ fn run_agent_pty_blocking(
|
||||
},
|
||||
tx,
|
||||
event_log,
|
||||
log_writer,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3514,4 +3583,38 @@ name = "qa"
|
||||
"story should be in 2_current/ or 3_qa/ after reconciliation"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_emit_event_writes_to_log_writer() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let root = tmp.path();
|
||||
|
||||
let log_writer =
|
||||
AgentLogWriter::new(root, "42_story_foo", "coder-1", "sess-emit").unwrap();
|
||||
let log_mutex = Mutex::new(log_writer);
|
||||
|
||||
let (tx, _rx) = broadcast::channel::<AgentEvent>(64);
|
||||
let event_log: Mutex<Vec<AgentEvent>> = Mutex::new(Vec::new());
|
||||
|
||||
let event = AgentEvent::Status {
|
||||
story_id: "42_story_foo".to_string(),
|
||||
agent_name: "coder-1".to_string(),
|
||||
status: "running".to_string(),
|
||||
};
|
||||
|
||||
emit_event(event, &tx, &event_log, Some(&log_mutex));
|
||||
|
||||
// Verify event was added to in-memory log
|
||||
let mem_events = event_log.lock().unwrap();
|
||||
assert_eq!(mem_events.len(), 1);
|
||||
drop(mem_events);
|
||||
|
||||
// Verify event was written to the log file
|
||||
let log_path =
|
||||
crate::agent_log::log_file_path(root, "42_story_foo", "coder-1", "sess-emit");
|
||||
let entries = crate::agent_log::read_log(&log_path).unwrap();
|
||||
assert_eq!(entries.len(), 1);
|
||||
assert_eq!(entries[0].event["type"], "status");
|
||||
assert_eq!(entries[0].event["status"], "running");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user