#![allow(dead_code)]
use std::path::PathBuf;
use std::sync::Arc;
use tokio::fs;
use super::agent_tool_utils::{extract_partial_result, finalize_agent_tool};
use super::load_agents_dir::AgentDefinition;
use super::run_agent::{
AgentOverrides, RunAgentParams, ToolContext, filter_incomplete_tool_calls, run_agent,
};
pub struct ResumeAgentResult {
pub agent_id: String,
pub description: String,
pub output_file: String,
}
struct AgentTranscript {
messages: Vec<serde_json::Value>,
content_replacements: Option<serde_json::Value>,
}
struct AgentMetadata {
agent_type: Option<String>,
worktree_path: Option<String>,
description: Option<String>,
}
pub async fn resume_agent_background(
agent_id: &str,
prompt: &str,
tool_context: ToolContext,
) -> Result<ResumeAgentResult, String> {
let start_time = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_millis() as u64;
let (transcript, meta) = load_transcript_and_metadata(agent_id).await?;
let resumed_messages = filter_whitespace_only_assistant_messages(
filter_orphaned_thinking_only_messages(filter_unresolved_tool_uses(&transcript.messages)),
);
let resumed_worktree_path = meta.worktree_path.as_ref().and_then(|p| {
let path = PathBuf::from(p);
if path.is_dir() {
let marker = path.join(".claude_resume_marker");
tokio::task::block_in_place(|| std::fs::write(&marker, "").ok());
Some(p.clone())
} else {
log::debug!(
"Resumed worktree {} no longer exists; falling back to parent cwd",
p
);
None
}
});
let selected_agent = select_agent_for_resume(&meta, &tool_context);
let ui_description = meta
.description
.clone()
.unwrap_or_else(|| "(resumed)".to_string());
let resolved_model = selected_agent
.model
.clone()
.unwrap_or_else(|| tool_context.main_loop_model.clone());
let mut prompt_messages = resumed_messages;
prompt_messages.push(serde_json::json!({
"type": "user",
"message": {
"content": [{"type": "text", "text": prompt}]
}
}));
let run_params = RunAgentParams {
agent_definition: selected_agent.clone(),
prompt_messages,
tool_context,
is_async: true,
override_params: None,
model: None,
max_turns: None,
fork_context_messages: None,
allowed_tools: None,
worktree_path: resumed_worktree_path.clone(),
description: meta.description.clone(),
};
let _result = run_agent(run_params).await?;
let output_file = get_task_output_path(agent_id);
Ok(ResumeAgentResult {
agent_id: agent_id.to_string(),
description: ui_description,
output_file,
})
}
async fn load_transcript_and_metadata(
agent_id: &str,
) -> Result<(AgentTranscript, AgentMetadata), String> {
let transcript_path = std::env::current_dir()
.map_err(|e| e.to_string())?
.join(".claude")
.join("subagents")
.join(agent_id)
.join("transcript.json");
let metadata_path = std::env::current_dir()
.map_err(|e| e.to_string())?
.join(".claude")
.join("subagents")
.join(agent_id)
.join("metadata.json");
let transcript_content = fs::read_to_string(&transcript_path)
.await
.map_err(|e| format!("Failed to read transcript: {}", e))?;
let messages: Vec<serde_json::Value> = serde_json::from_str(&transcript_content)
.map_err(|e| format!("Failed to parse transcript: {}", e))?;
let content_replacements = None;
let meta = if fs::metadata(&metadata_path).await.is_ok() {
let meta_content = fs::read_to_string(&metadata_path)
.await
.map_err(|e| format!("Failed to read metadata: {}", e))?;
let meta_json: serde_json::Value = serde_json::from_str(&meta_content)
.map_err(|e| format!("Failed to parse metadata: {}", e))?;
AgentMetadata {
agent_type: meta_json
.get("agentType")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
worktree_path: meta_json
.get("worktreePath")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
description: meta_json
.get("description")
.and_then(|v| v.as_str())
.map(|s| s.to_string()),
}
} else {
AgentMetadata {
agent_type: None,
worktree_path: None,
description: None,
}
};
Ok((
AgentTranscript {
messages,
content_replacements,
},
meta,
))
}
fn filter_whitespace_only_assistant_messages(
messages: Vec<serde_json::Value>,
) -> Vec<serde_json::Value> {
messages
.into_iter()
.filter(|msg| {
if msg.get("type").and_then(|t| t.as_str()) != Some("assistant") {
return true;
}
if let Some(content) = msg.get("message").and_then(|m| m.get("content")) {
if let Some(arr) = content.as_array() {
for block in arr {
if let Some(text) = block.get("text").and_then(|v| v.as_str()) {
if !text.trim().is_empty() {
return true;
}
}
if block.get("type").and_then(|t| t.as_str()) != Some("text") {
return true; }
}
return false; }
}
true })
.collect()
}
fn filter_orphaned_thinking_only_messages(
messages: Vec<serde_json::Value>,
) -> Vec<serde_json::Value> {
messages
.into_iter()
.filter(|msg| {
if msg.get("type").and_then(|t| t.as_str()) != Some("assistant") {
return true;
}
if let Some(content) = msg.get("message").and_then(|m| m.get("content")) {
if let Some(arr) = content.as_array() {
return arr.iter().any(|block| {
block.get("type").and_then(|t| t.as_str()) == Some("tool_use")
|| block.get("type").and_then(|t| t.as_str()) == Some("text")
});
}
}
true
})
.collect()
}
fn filter_unresolved_tool_uses(messages: &[serde_json::Value]) -> Vec<serde_json::Value> {
let messages = messages.to_vec();
let mut tool_use_ids_with_results = std::collections::HashSet::new();
for msg in &messages {
if msg.get("type").and_then(|t| t.as_str()) == Some("user") {
if let Some(content) = msg.get("message").and_then(|m| m.get("content")) {
if let Some(arr) = content.as_array() {
for block in arr {
if block.get("type").and_then(|t| t.as_str()) == Some("tool_result") {
if let Some(id) = block.get("tool_use_id").and_then(|v| v.as_str()) {
tool_use_ids_with_results.insert(id.to_string());
}
}
}
}
}
}
}
messages
.into_iter()
.filter(|msg| {
if msg.get("type").and_then(|t| t.as_str()) != Some("assistant") {
return true;
}
if let Some(content) = msg.get("message").and_then(|m| m.get("content")) {
if let Some(arr) = content.as_array() {
let tool_uses: Vec<_> = arr
.iter()
.filter(|b| b.get("type").and_then(|t| t.as_str()) == Some("tool_use"))
.collect();
if tool_uses.is_empty() {
return true; }
return tool_uses.iter().all(|block| {
block
.get("id")
.and_then(|v| v.as_str())
.is_some_and(|id| tool_use_ids_with_results.contains(id))
});
}
}
true
})
.collect()
}
fn select_agent_for_resume(meta: &AgentMetadata, tool_context: &ToolContext) -> AgentDefinition {
if let Some(ref agent_type) = meta.agent_type {
if let Some(found) = tool_context
.agent_definitions
.iter()
.find(|a| &a.agent_type == agent_type)
{
return found.clone();
}
}
super::built_in_agents::general_purpose_agent()
}
fn get_task_output_path(agent_id: &str) -> String {
std::env::current_dir()
.unwrap_or_default()
.join(".claude")
.join("tasks")
.join(format!("{}.output", agent_id))
.to_string_lossy()
.to_string()
}
mod general_purpose_fallback {
use std::sync::Arc;
use super::AgentDefinition;
pub fn general_purpose_agent() -> AgentDefinition {
AgentDefinition {
agent_type: "general-purpose".to_string(),
when_to_use: "General-purpose agent for multi-step tasks".to_string(),
tools: vec!["*".to_string()],
disallowed_tools: vec![],
source: "built-in".to_string(),
base_dir: "built-in".to_string(),
get_system_prompt: Arc::new(|| "You are a helpful assistant.".to_string()),
model: None,
max_turns: None,
permission_mode: None,
effort: None,
color: None,
mcp_servers: vec![],
hooks: None,
skills: vec![],
background: false,
initial_prompt: None,
memory: None,
isolation: None,
required_mcp_servers: vec![],
omit_claude_md: false,
critical_system_reminder_experimental: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_filter_whitespace_only_messages() {
let messages = vec![
serde_json::json!({
"type": "assistant",
"message": {
"content": [{"type": "text", "text": " \n "}]
}
}),
serde_json::json!({
"type": "assistant",
"message": {
"content": [{"type": "text", "text": "hello"}]
}
}),
];
let filtered = filter_whitespace_only_assistant_messages(messages);
assert_eq!(filtered.len(), 1);
}
#[test]
fn test_filter_orphaned_thinking_messages() {
let messages = vec![
serde_json::json!({
"type": "assistant",
"message": {
"content": [] }
}),
serde_json::json!({
"type": "assistant",
"message": {
"content": [{"type": "tool_use", "id": "1", "name": "Bash"}]
}
}),
];
let filtered = filter_orphaned_thinking_only_messages(messages);
assert_eq!(filtered.len(), 1); }
}