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.
This commit is contained in:
ZhenYi 2026-04-29 09:03:29 +08:00
parent 30822bbd7d
commit 03f97c9221

View File

@ -685,17 +685,30 @@ impl ChatService {
for call in &calls { for call in &calls {
let start = std::time::Instant::now(); let start = std::time::Instant::now();
let executor = crate::tool::ToolExecutor::new();
// Use select! loop to send heartbeat chunks at 30s intervals // Spawn tool execution in a separate task to avoid blocking the
// during long tool execution, resetting the frontend streaming timer. // tokio worker thread (git2 operations are synchronous).
let fut = executor.execute_batch(vec![call.clone()], &mut ctx); // This allows the heartbeat timer to fire independently.
tokio::pin!(fut); 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 { let results = loop {
tokio::select! { tokio::select! {
result = fut.as_mut() => break result, res = &mut result_rx => {
_ = tokio::time::sleep(std::time::Duration::from_secs(30)) => { 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 { on_chunk(AiStreamChunk {
content: String::new(), content: String::new(),
done: false, done: false,
@ -908,6 +921,17 @@ impl ChatService {
async fn build_messages(&self, request: &AiRequest) -> Result<Vec<ChatRequestMessage>> { async fn build_messages(&self, request: &AiRequest) -> Result<Vec<ChatRequestMessage>> {
let mut messages = Vec::new(); 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(); let mut processed_history = Vec::new();
if let Some(compact_service) = &self.compact_service { if let Some(compact_service) = &self.compact_service {
let config = CompactConfig::default(); let config = CompactConfig::default();