fix(subagent): keep agent_eval recoverable after child termination

Keep agent_eval recoverable after a child session terminates by preserving the available transcript result instead of losing the final output.
This commit is contained in:
Hunter Bown
2026-05-23 13:05:36 -05:00
committed by GitHub
parent b7e31a734b
commit c09f5bf039
2 changed files with 87 additions and 5 deletions
+30 -5
View File
@@ -2421,11 +2421,35 @@ impl ToolSpec for AgentEvalTool {
.map_err(|e| ToolError::execution_failed(e.to_string()))?
};
// Track whether a supplied follow-up message actually reached the
// child. A completed/failed/cancelled session cannot accept input, but
// that must NOT abort the whole call: the parent still needs the
// session projection (and its `transcript_handle`) to retrieve the
// child's full output. Hard-failing here was #1738 — "agent_eval on a
// completed session returns 'not running', no way to recover the full
// child output".
let mut message_delivery: Option<Value> = None;
if let Some(message) = message {
let mut manager = self.manager.write().await;
manager
.send_input(&agent_id, message, interrupt)
.map_err(|e| ToolError::execution_failed(e.to_string()))?;
let terminal = {
let manager = self.manager.read().await;
manager
.get_result(&agent_id)
.map(|snap| snap.status != SubAgentStatus::Running)
.unwrap_or(false)
};
if terminal {
message_delivery = Some(json!({
"delivered": false,
"reason": "session already terminated; follow-up not delivered",
"recover_full_output": "read the returned transcript_handle with handle_read"
}));
} else {
let mut manager = self.manager.write().await;
manager
.send_input(&agent_id, message, interrupt)
.map_err(|e| ToolError::execution_failed(e.to_string()))?;
message_delivery = Some(json!({ "delivered": true }));
}
}
let (snapshot, timed_out) = if block {
@@ -2448,7 +2472,8 @@ impl ToolSpec for AgentEvalTool {
"timed_out": timed_out,
"terminal": projection.terminal,
"context_mode": projection.context_mode,
"timeout_ms": timeout_ms
"timeout_ms": timeout_ms,
"message_delivery": message_delivery
}));
Ok(result)
}
+57
View File
@@ -825,6 +825,63 @@ async fn test_wait_for_result_reports_timeout_when_still_running() {
assert_eq!(snapshot.status, SubAgentStatus::Running);
}
// Regression for #1738: agent_eval on a terminated session must not
// hard-fail with "not running" when a follow-up message is supplied. The
// parent still needs the projection (and its transcript_handle) to recover
// the child's full output.
#[tokio::test]
async fn agent_eval_on_completed_session_returns_full_projection_not_running_error() {
let manager = Arc::new(RwLock::new(SubAgentManager::new(PathBuf::from("."), 1)));
let (input_tx, _input_rx) = mpsc::unbounded_channel();
let mut agent = SubAgent::new(
SubAgentType::Explore,
"analyze 14 issues".to_string(),
make_assignment(),
"deepseek-v4-flash".to_string(),
Some("Blue".to_string()),
Some(vec!["read_file".to_string()]),
input_tx,
"boot_test".to_string(),
);
let full_output = "Per-issue analysis:\n".to_string() + &"detail line\n".repeat(400);
agent.status = SubAgentStatus::Completed;
agent.result = Some(full_output.clone());
let agent_id = agent.id.clone();
{
let mut guard = manager.write().await;
guard.agents.insert(agent_id.clone(), agent);
}
let ctx = ToolContext::new(".");
let tool = AgentEvalTool::new(manager.clone());
let result = tool
.execute(
json!({
"agent_id": agent_id,
"message": "give me the full per-issue breakdown",
"block": false
}),
&ctx,
)
.await
.expect("agent_eval on a completed session must not error");
let meta = result.metadata.expect("metadata present");
assert_eq!(meta["terminal"], json!(true));
assert_eq!(meta["message_delivery"]["delivered"], json!(false));
let projection: SubAgentSessionProjection =
serde_json::from_str(&result.content).expect("projection deserializes");
assert_eq!(projection.status, "completed");
assert_eq!(projection.transcript_handle.kind, "var_handle");
// The full, untruncated child output survives in the snapshot the
// transcript_handle points at.
assert_eq!(
projection.snapshot.result.as_deref(),
Some(full_output.as_str())
);
}
#[tokio::test]
async fn test_running_count_counts_only_agents_with_live_task_handles() {
let mut manager = SubAgentManager::new(PathBuf::from("."), 1);