From 03f97c9221e41acbb0dba9594d5142099327fb1e Mon Sep 17 00:00:00 2001 From: ZhenYi <434836402@qq.com> Date: Wed, 29 Apr 2026 09:03:29 +0800 Subject: [PATCH] fix(agent): spawn tool execution in separate task for heartbeat Move tool execution to a spawned task so synchronous git2 operations don't block the tokio worker thread, allowing heartbeat chunks to be sent every 10s during long tool execution. Also add analysis-first reasoning prompt to system messages. --- libs/agent/chat/service.rs | 38 +++++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/libs/agent/chat/service.rs b/libs/agent/chat/service.rs index a65021d..de48f11 100644 --- a/libs/agent/chat/service.rs +++ b/libs/agent/chat/service.rs @@ -685,17 +685,30 @@ impl ChatService { for call in &calls { let start = std::time::Instant::now(); - let executor = crate::tool::ToolExecutor::new(); - // Use select! loop to send heartbeat chunks at 30s intervals - // during long tool execution, resetting the frontend streaming timer. - let fut = executor.execute_batch(vec![call.clone()], &mut ctx); - tokio::pin!(fut); + // Spawn tool execution in a separate task to avoid blocking the + // tokio worker thread (git2 operations are synchronous). + // This allows the heartbeat timer to fire independently. + let call_clone = call.clone(); + let mut ctx_clone = ctx.clone(); + let (result_tx, mut result_rx) = tokio::sync::oneshot::channel(); + tokio::spawn(async move { + let executor = crate::tool::ToolExecutor::new(); + let res = executor.execute_batch(vec![call_clone], &mut ctx_clone).await; + let _ = result_tx.send(res); + }); + // Send heartbeats every 10s until tool execution completes + let heartbeat_dur = std::time::Duration::from_secs(10); let results = loop { tokio::select! { - result = fut.as_mut() => break result, - _ = tokio::time::sleep(std::time::Duration::from_secs(30)) => { + res = &mut result_rx => { + match res { + Ok(inner) => break inner, + Err(_) => break Err(crate::tool::ToolError::ExecutionError("tool task cancelled".into())), + } + }, + _ = tokio::time::sleep(heartbeat_dur) => { on_chunk(AiStreamChunk { content: String::new(), done: false, @@ -908,6 +921,17 @@ impl ChatService { async fn build_messages(&self, request: &AiRequest) -> Result> { let mut messages = Vec::new(); + // Core reasoning instruction — prioritize analysis before answering. + messages.push(ChatRequestMessage::system( + "When receiving a question or problem, follow this reasoning process:\n\ + 1. ANALYZE: Break down the question. Identify what is being asked, what context is available, and what information is missing.\n\ + 2. GATHER: Use available tools (repository search, file reading, etc.) to collect relevant information before answering.\n\ + 3. REASON: Synthesize the gathered information. Consider edge cases and potential issues.\n\ + 4. ANSWER: Provide a clear, actionable answer based on your analysis.\n\ + \n\ + Do NOT guess or assume when tools can provide concrete answers. Always verify claims against actual code or documentation.".to_string() + )); + let mut processed_history = Vec::new(); if let Some(compact_service) = &self.compact_service { let config = CompactConfig::default();