use serde_json::Value;
use vtcode_core::tools::registry::ToolExecutionError;
#[derive(Debug)]
pub(crate) enum ToolExecutionStatus {
Success {
output: Value,
stdout: Option<String>,
modified_files: Vec<String>,
command_success: bool,
},
Failure {
error: ToolExecutionError,
},
Timeout {
error: ToolExecutionError,
},
Cancelled,
}
impl ToolExecutionStatus {
pub(crate) fn error(&self) -> Option<&ToolExecutionError> {
match self {
ToolExecutionStatus::Failure { error } | ToolExecutionStatus::Timeout { error } => {
Some(error)
}
ToolExecutionStatus::Success { .. } | ToolExecutionStatus::Cancelled => None,
}
}
}
pub(crate) struct ToolPipelineOutcome {
pub status: ToolExecutionStatus,
pub command_success: bool,
}
impl ToolPipelineOutcome {
pub(crate) fn from_status(status: ToolExecutionStatus) -> Self {
let command_success = match &status {
ToolExecutionStatus::Success {
command_success, ..
} => *command_success,
_ => false,
};
ToolPipelineOutcome {
status,
command_success,
}
}
pub(crate) fn modified_files(&self) -> &[String] {
match &self.status {
ToolExecutionStatus::Success { modified_files, .. } => modified_files,
_ => &[],
}
}
pub(crate) fn modified_files_mut(&mut self) -> Option<&mut Vec<String>> {
match &mut self.status {
ToolExecutionStatus::Success { modified_files, .. } => Some(modified_files),
_ => None,
}
}
pub(crate) fn set_command_success(&mut self, command_success: bool) {
self.command_success = command_success;
if let ToolExecutionStatus::Success {
command_success: status_success,
..
} = &mut self.status
{
*status_success = command_success;
}
}
}
#[derive(Debug)]
pub(crate) struct ToolBatchOutcome {
pub entries: Vec<ToolBatchEntry>,
}
#[derive(Debug)]
pub(crate) struct ToolBatchEntry {
pub result: ToolBatchResult,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum ToolBatchResult {
Success,
Failure,
Timeout,
Cancelled,
}
impl From<&ToolExecutionStatus> for ToolBatchResult {
fn from(status: &ToolExecutionStatus) -> Self {
match status {
ToolExecutionStatus::Success { .. } => Self::Success,
ToolExecutionStatus::Failure { .. } => Self::Failure,
ToolExecutionStatus::Timeout { .. } => Self::Timeout,
ToolExecutionStatus::Cancelled => Self::Cancelled,
}
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct ToolBatchStats {
pub total: usize,
pub succeeded: usize,
pub failed: usize,
pub timed_out: usize,
pub cancelled: usize,
}
impl ToolBatchOutcome {
pub(crate) fn new() -> Self {
Self {
entries: Vec::new(),
}
}
pub(crate) fn record(&mut self, status: &ToolExecutionStatus) {
self.entries.push(ToolBatchEntry {
result: ToolBatchResult::from(status),
});
}
pub(crate) fn stats(&self) -> ToolBatchStats {
let mut s = ToolBatchStats {
total: self.entries.len(),
succeeded: 0,
failed: 0,
timed_out: 0,
cancelled: 0,
};
for entry in &self.entries {
match entry.result {
ToolBatchResult::Success => s.succeeded += 1,
ToolBatchResult::Failure => s.failed += 1,
ToolBatchResult::Timeout => s.timed_out += 1,
ToolBatchResult::Cancelled => s.cancelled += 1,
}
}
s
}
pub(crate) fn is_partial_success(&self) -> bool {
let s = self.stats();
s.succeeded > 0 && (s.failed > 0 || s.timed_out > 0)
}
pub(crate) fn summary_line(&self) -> String {
let s = self.stats();
if s.total == 0 {
return "no tools executed".to_string();
}
if s.succeeded == s.total {
return format!("all {} tools succeeded", s.total);
}
let mut parts = Vec::new();
if s.succeeded > 0 {
parts.push(format!("{} succeeded", s.succeeded));
}
if s.failed > 0 {
parts.push(format!("{} failed", s.failed));
}
if s.timed_out > 0 {
parts.push(format!("{} timed out", s.timed_out));
}
if s.cancelled > 0 {
parts.push(format!("{} cancelled", s.cancelled));
}
format!("{}/{} tools: {}", s.total, s.total, parts.join(", "))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_batch_stats() {
let batch = ToolBatchOutcome::new();
let s = batch.stats();
assert_eq!(s.total, 0);
assert!(!batch.is_partial_success());
assert_eq!(batch.summary_line(), "no tools executed");
}
#[test]
fn all_success_batch() {
let mut batch = ToolBatchOutcome::new();
let success = ToolExecutionStatus::Success {
output: serde_json::json!("ok"),
stdout: None,
modified_files: vec![],
command_success: true,
};
batch.record(&success);
batch.record(&success);
let s = batch.stats();
assert_eq!(s.total, 2);
assert_eq!(s.succeeded, 2);
assert!(!batch.is_partial_success());
assert!(batch.summary_line().contains("all 2 tools succeeded"));
}
#[test]
fn partial_failure_batch() {
let mut batch = ToolBatchOutcome::new();
let success = ToolExecutionStatus::Success {
output: serde_json::json!("ok"),
stdout: None,
modified_files: vec![],
command_success: true,
};
let failure = ToolExecutionStatus::Failure {
error: ToolExecutionError::new(
"tool".to_string(),
vtcode_core::tools::registry::ToolErrorType::PermissionDenied,
"permission denied".to_string(),
),
};
batch.record(&success);
batch.record(&failure);
assert!(batch.is_partial_success());
let s = batch.stats();
assert_eq!(s.succeeded, 1);
assert_eq!(s.failed, 1);
assert!(batch.summary_line().contains("1 succeeded"));
assert!(batch.summary_line().contains("1 failed"));
}
#[test]
fn all_failure_not_partial_success() {
let mut batch = ToolBatchOutcome::new();
let failure = ToolExecutionStatus::Failure {
error: ToolExecutionError::new(
"tool".to_string(),
vtcode_core::tools::registry::ToolErrorType::ExecutionError,
"boom".to_string(),
),
};
batch.record(&failure);
assert!(!batch.is_partial_success());
}
#[test]
fn timeout_entry_tracked() {
let mut batch = ToolBatchOutcome::new();
let timeout = ToolExecutionStatus::Timeout {
error: ToolExecutionError::new(
"slow_tool".to_string(),
vtcode_core::tools::registry::ToolErrorType::Timeout,
"timed out after 30s".to_string(),
),
};
batch.record(&timeout);
let s = batch.stats();
assert_eq!(s.timed_out, 1);
}
}