huskies: merge 540_bug_get_agent_output_mcp_tool_returns_no_agent_for_exited_agents_instead_of_reading_session_logs_from_disk

This commit is contained in:
dave
2026-04-11 16:30:13 +00:00
parent a9a1852422
commit 6998275331
2 changed files with 73 additions and 138 deletions
+73 -5
View File
@@ -219,10 +219,42 @@ fn handle_agent_output_sse(
let mut rx = match ctx.agents.subscribe(&story_id, &agent_name) {
Ok(rx) => rx,
Err(e) => return to_sse_response(JsonRpcResponse::success(
id,
json!({ "content": [{"type": "text", "text": e}], "isError": true }),
)),
Err(_) => {
// Agent not in pool (exited or never started) — fall back to disk logs.
let text = if let Ok(project_root) = ctx.agents.get_project_root(&ctx.state) {
use crate::agent_log;
let log_files = agent_log::list_story_log_files(
&project_root,
&story_id,
Some(&agent_name),
);
if log_files.is_empty() {
format!("No log files found for story '{story_id}' agent '{agent_name}'.")
} else {
let mut all_lines: Vec<String> = Vec::new();
for path in &log_files {
let file_name =
path.file_name().and_then(|n| n.to_str()).unwrap_or("?");
all_lines.push(format!(
"=== {} ===",
file_name.trim_end_matches(".log")
));
match agent_log::read_log_as_readable_lines(path) {
Ok(lines) => all_lines.extend(lines),
Err(e) => all_lines.push(format!("[ERROR reading log: {e}]")),
}
all_lines.push(String::new());
}
all_lines.join("\n")
}
} else {
format!("No log files found for story '{story_id}' agent '{agent_name}'.")
};
return to_sse_response(JsonRpcResponse::success(
id,
json!({ "content": [{"type": "text", "text": text}] }),
));
}
};
let final_id = id;
@@ -1799,7 +1831,8 @@ mod tests {
}
#[tokio::test]
async fn mcp_post_sse_get_agent_output_no_agent_returns_sse_error() {
async fn mcp_post_sse_get_agent_output_no_agent_no_logs_returns_not_found() {
// Agent not in pool and no log files → SSE success with "No log files found" message.
let tmp = tempfile::tempdir().unwrap();
let ctx = std::sync::Arc::new(test_ctx(tmp.path()));
let cli = poem::test::TestClient::new(test_mcp_app(ctx));
@@ -1816,5 +1849,40 @@ mod tests {
);
let body = resp.0.into_body().into_string().await.unwrap();
assert!(body.contains("data:"), "expected SSE data prefix: {body}");
// Must NOT return isError — should be a success result with "No log files found"
assert!(!body.contains("isError"), "expected no isError for missing agent: {body}");
assert!(body.contains("No log files found"), "expected not-found message: {body}");
}
#[tokio::test]
async fn mcp_post_sse_get_agent_output_exited_agent_reads_disk_logs() {
use crate::agent_log::AgentLogWriter;
use crate::agents::AgentEvent;
// Agent has exited (not in pool) but wrote logs to disk.
let tmp = tempfile::tempdir().unwrap();
let root = tmp.path();
let mut writer =
AgentLogWriter::new(root, "42_story_foo", "coder-1", "sess-sse").unwrap();
writer
.write_event(&AgentEvent::Output {
story_id: "42_story_foo".to_string(),
agent_name: "coder-1".to_string(),
text: "disk output".to_string(),
})
.unwrap();
drop(writer);
let ctx = std::sync::Arc::new(test_ctx(root));
let cli = poem::test::TestClient::new(test_mcp_app(ctx));
let resp = cli
.post("/mcp")
.header("content-type", "application/json")
.header("accept", "text/event-stream")
.body(r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"get_agent_output","arguments":{"story_id":"42_story_foo","agent_name":"coder-1"}}}"#)
.send()
.await;
let body = resp.0.into_body().into_string().await.unwrap();
assert!(body.contains("disk output"), "expected disk log content in SSE response: {body}");
assert!(!body.contains("isError"), "expected no error for exited agent with logs: {body}");
}
}