feat(summon): add structured metadata to task load results (#9521)

Signed-off-by: Douwe M Osinga <douwe@sidewalklabs.com>
Co-authored-by: Douwe M Osinga <douwe@sidewalklabs.com>
This commit is contained in:
jeffa-block
2026-06-17 00:48:37 +10:00
committed by GitHub
parent 93285db7c9
commit 4602a88446
@@ -83,6 +83,15 @@ pub struct CompletedTask {
pub completed_at: Instant,
}
/// Result from handle_load_task_result with structured metadata for the caller
#[derive(Debug)]
struct TaskLoadResult {
content: Vec<Content>,
status: &'static str,
turns: Option<u32>,
duration_secs: Option<u64>,
}
#[derive(Debug, Deserialize)]
struct AgentMetadata {
name: String,
@@ -790,13 +799,29 @@ impl SummonClient {
let name = source_name.unwrap();
if is_session_id(name) {
let content = self.handle_load_task_result(name, cancel).await?;
let task_result = self.handle_load_task_result(name, cancel).await?;
let mut meta = Meta::new();
meta.0.insert(
"subagent_session_id".to_string(),
serde_json::Value::String(name.to_string()),
);
return Ok(CallToolResult::success(content).with_meta(Some(meta)));
meta.0.insert(
"task_status".to_string(),
serde_json::Value::String(task_result.status.to_string()),
);
if let Some(turns) = task_result.turns {
meta.0.insert(
"turns_taken".to_string(),
serde_json::Value::Number(turns.into()),
);
}
if let Some(secs) = task_result.duration_secs {
meta.0.insert(
"duration_secs".to_string(),
serde_json::Value::Number(secs.into()),
);
}
return Ok(CallToolResult::success(task_result.content).with_meta(Some(meta)));
}
self.handle_load_source(session_id, name, &working_dir)
@@ -808,33 +833,42 @@ impl SummonClient {
&self,
task_id: &str,
cancel: bool,
) -> Result<Vec<Content>, String> {
) -> Result<TaskLoadResult, String> {
let mut completed = self.completed_tasks.lock().await;
if let Some(task) = completed.remove(task_id) {
let status = if task.result.is_ok() {
"✓ Completed"
} else {
"✗ Failed"
let status_key = match &task.result {
Ok(_) => "completed",
Err(e) if e.starts_with("Task panicked:") => "panicked",
Err(_) => "failed",
};
let status = match status_key {
"completed" => "✓ Completed",
"panicked" => "✗ Panicked",
_ => "✗ Failed",
};
let output = match task.result {
Ok(output) => output,
Err(error) => format!("Error: {}", error),
};
return Ok(vec![Content::text(format!(
"# Background Task Result: {}\n\n\
**Task:** {}\n\
**Status:** {}\n\
**Duration:** {} ({} turns)\n\n\
## Output\n\n{}",
task_id,
task.description,
status,
round_duration(task.duration),
task.turns_taken,
output
))]);
return Ok(TaskLoadResult {
content: vec![Content::text(format!(
"# Background Task Result: {}\n\n\
**Task:** {}\n\
**Status:** {}\n\
**Duration:** {} ({} turns)\n\n\
## Output\n\n{}",
task_id,
task.description,
status,
round_duration(task.duration),
task.turns_taken,
output
))],
status: status_key,
turns: Some(task.turns_taken),
duration_secs: Some(task.duration.as_secs()),
});
}
drop(completed);
@@ -865,18 +899,23 @@ impl SummonClient {
}
};
return Ok(vec![Content::text(format!(
"# Background Task Result: {}\n\n\
**Task:** {}\n\
**Status:** ⊘ Cancelled\n\
**Duration:** {} ({} turns)\n\n\
## Output\n\n{}",
task_id,
task.description,
round_duration(duration),
turns_taken,
output
))]);
return Ok(TaskLoadResult {
content: vec![Content::text(format!(
"# Background Task Result: {}\n\n\
**Task:** {}\n\
**Status:** ⊘ Cancelled\n\
**Duration:** {} ({} turns)\n\n\
## Output\n\n{}",
task_id,
task.description,
round_duration(duration),
turns_taken,
output
))],
status: "cancelled",
turns: Some(turns_taken),
duration_secs: Some(duration.as_secs()),
});
}
// Wait for the running task to complete, keeping the tool call
@@ -899,24 +938,37 @@ impl SummonClient {
tokio::select! {
result = &mut task.handle => {
let output = match result {
Ok(Ok(s)) => s,
Ok(Err(e)) => format!("Error: {}", e),
Err(e) => format!("Task panicked: {}", e),
let (output, status_key) = match result {
Ok(Ok(s)) => (s, "completed"),
Ok(Err(e)) => (format!("Error: {}", e), "failed"),
Err(e) => (format!("Task panicked: {}", e), "panicked"),
};
return Ok(vec![Content::text(format!(
"# Background Task Result: {}\n\n\
**Task:** {}\n\
**Status:** ✓ Completed\n\
**Duration:** {} ({} turns)\n\n\
## Output\n\n{}",
task_id,
task.description,
round_duration(task.started_at.elapsed()),
task.turns.load(Ordering::Relaxed),
output
))]);
let turns_taken = task.turns.load(Ordering::Relaxed);
let elapsed = task.started_at.elapsed();
let status_display = match status_key {
"completed" => "✓ Completed",
"panicked" => "✗ Panicked",
_ => "✗ Failed",
};
return Ok(TaskLoadResult {
content: vec![Content::text(format!(
"# Background Task Result: {}\n\n\
**Task:** {}\n\
**Status:** {}\n\
**Duration:** {} ({} turns)\n\n\
## Output\n\n{}",
task_id,
task.description,
status_display,
round_duration(elapsed),
turns_taken,
output
))],
status: status_key,
turns: Some(turns_taken),
duration_secs: Some(elapsed.as_secs()),
});
}
_ = tokio::time::sleep(Duration::from_secs(300)) => {
self.background_tasks.lock().await.insert(task_id.to_string(), task);
@@ -2476,7 +2528,7 @@ You review code."#;
.handle_load_task_result("20260204_1", false)
.await
.expect("load should wait and return result");
let text = extract_text(&result[0]);
let text = extract_text(&result.content[0]);
assert!(text.contains("Completed"));
assert!(text.contains("done"));
@@ -2538,13 +2590,15 @@ You review code."#;
.handle_load_task_result("20260204_2", false)
.await
.unwrap();
let text = extract_text(&result[0]);
let text = extract_text(&result.content[0]);
assert!(text.contains("20260204_2"));
assert!(text.contains("Successful task"));
assert!(text.contains("✓ Completed"));
assert!(text.contains("1m"));
assert!(text.contains("5 turns"));
assert!(text.contains("Task completed successfully with output"));
assert_eq!(result.status, "completed");
assert_eq!(result.turns, Some(5));
assert!(!client
.completed_tasks
@@ -2556,9 +2610,10 @@ You review code."#;
.handle_load_task_result("20260204_3", false)
.await
.unwrap();
let text = extract_text(&result[0]);
let text = extract_text(&result.content[0]);
assert!(text.contains("✗ Failed"));
assert!(text.contains("Error: Something went wrong"));
assert_eq!(result.status, "failed");
let result = client.handle_load_task_result("20260204_3", false).await;
assert!(result.is_err());
@@ -2597,10 +2652,12 @@ You review code."#;
.handle_load_task_result("20260204_1", true)
.await
.unwrap();
let text = extract_text(&result[0]);
let text = extract_text(&result.content[0]);
assert!(text.contains("Cancelled"));
assert!(text.contains("20260204_1"));
assert!(text.contains("Cancellable task"));
assert_eq!(result.status, "cancelled");
assert_eq!(result.turns, Some(3));
assert!(token.is_cancelled());
assert!(!client
.background_tasks