feat: step artifacts framework
- Add Artifact type to Step (name, path, artifact_type, description) - step_done tool accepts optional artifacts parameter - Save artifacts to step_artifacts DB table - Display artifacts in frontend PlanSection (tag style) - Show artifacts in step context for sub-agents and coordinator - Add LLM client retry with exponential backoff
This commit is contained in:
85
src/llm.rs
85
src/llm.rs
@@ -93,7 +93,11 @@ pub struct ChatChoice {
|
||||
impl LlmClient {
|
||||
pub fn new(config: &LlmConfig) -> Self {
|
||||
Self {
|
||||
client: reqwest::Client::new(),
|
||||
client: reqwest::Client::builder()
|
||||
.timeout(std::time::Duration::from_secs(120))
|
||||
.connect_timeout(std::time::Duration::from_secs(10))
|
||||
.build()
|
||||
.expect("Failed to build HTTP client"),
|
||||
config: config.clone(),
|
||||
}
|
||||
}
|
||||
@@ -106,34 +110,65 @@ impl LlmClient {
|
||||
.unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Chat with tool definitions — returns full response for tool-calling loop
|
||||
/// Chat with tool definitions — returns full response for tool-calling loop.
|
||||
/// Retries up to 3 times with exponential backoff on transient errors.
|
||||
pub async fn chat_with_tools(&self, messages: Vec<ChatMessage>, tools: &[Tool]) -> anyhow::Result<ChatResponse> {
|
||||
let url = format!("{}/chat/completions", self.config.base_url);
|
||||
tracing::debug!("LLM request to {} model={} messages={} tools={}", url, self.config.model, messages.len(), tools.len());
|
||||
let http_resp = self.client
|
||||
.post(&url)
|
||||
.header("Authorization", format!("Bearer {}", self.config.api_key))
|
||||
.json(&ChatRequest {
|
||||
model: self.config.model.clone(),
|
||||
messages,
|
||||
tools: tools.to_vec(),
|
||||
})
|
||||
.send()
|
||||
.await?;
|
||||
let max_retries = 3u32;
|
||||
let mut last_err = None;
|
||||
let tools_vec = tools.to_vec();
|
||||
|
||||
let status = http_resp.status();
|
||||
if !status.is_success() {
|
||||
let body = http_resp.text().await.unwrap_or_default();
|
||||
tracing::error!("LLM API error {}: {}", status, &body[..body.len().min(500)]);
|
||||
anyhow::bail!("LLM API error {}: {}", status, body);
|
||||
for attempt in 0..max_retries {
|
||||
if attempt > 0 {
|
||||
let delay = std::time::Duration::from_secs(2u64.pow(attempt));
|
||||
tracing::warn!("LLM retry #{} after {}s", attempt, delay.as_secs());
|
||||
tokio::time::sleep(delay).await;
|
||||
}
|
||||
|
||||
tracing::debug!("LLM request to {} model={} messages={} tools={} attempt={}", url, self.config.model, messages.len(), tools_vec.len(), attempt + 1);
|
||||
let result = self.client
|
||||
.post(&url)
|
||||
.header("Authorization", format!("Bearer {}", self.config.api_key))
|
||||
.json(&ChatRequest {
|
||||
model: self.config.model.clone(),
|
||||
messages: messages.clone(),
|
||||
tools: tools_vec.clone(),
|
||||
})
|
||||
.send()
|
||||
.await;
|
||||
|
||||
let http_resp = match result {
|
||||
Ok(r) => r,
|
||||
Err(e) => {
|
||||
tracing::warn!("LLM request error (attempt {}): {}", attempt + 1, e);
|
||||
last_err = Some(anyhow::anyhow!("{}", e));
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let status = http_resp.status();
|
||||
if status.is_server_error() || status.as_u16() == 429 {
|
||||
let body = http_resp.text().await.unwrap_or_default();
|
||||
tracing::warn!("LLM API error {} (attempt {}): {}", status, attempt + 1, &body[..body.len().min(200)]);
|
||||
last_err = Some(anyhow::anyhow!("LLM API error {}: {}", status, body));
|
||||
continue;
|
||||
}
|
||||
|
||||
if !status.is_success() {
|
||||
let body = http_resp.text().await.unwrap_or_default();
|
||||
tracing::error!("LLM API error {}: {}", status, &body[..body.len().min(500)]);
|
||||
anyhow::bail!("LLM API error {}: {}", status, body);
|
||||
}
|
||||
|
||||
let body = http_resp.text().await?;
|
||||
let resp: ChatResponse = serde_json::from_str(&body).map_err(|e| {
|
||||
tracing::error!("LLM response parse error: {}. Body: {}", e, &body[..body.len().min(500)]);
|
||||
anyhow::anyhow!("Failed to parse LLM response: {}", e)
|
||||
})?;
|
||||
|
||||
return Ok(resp);
|
||||
}
|
||||
|
||||
let body = http_resp.text().await?;
|
||||
let resp: ChatResponse = serde_json::from_str(&body).map_err(|e| {
|
||||
tracing::error!("LLM response parse error: {}. Body: {}", e, &body[..body.len().min(500)]);
|
||||
anyhow::anyhow!("Failed to parse LLM response: {}", e)
|
||||
})?;
|
||||
|
||||
Ok(resp)
|
||||
Err(last_err.unwrap_or_else(|| anyhow::anyhow!("LLM call failed after {} retries", max_retries)))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user