vv-agent 0.11.0

VectorVein agent runtime, SDK, CLI, tools, and workspace backends
Documentation
use std::sync::Arc;

use serde_json::Value;

use crate::runtime::background_sessions::background_session_manager;
use crate::tools::base::{ToolContext, ToolSpec};
use crate::tools::common::{string_arg, tool_error_with_code, tool_result_with_metadata};
use crate::types::{
    Metadata, ToolArguments, ToolArtifactRef, ToolDirective, ToolExecutionResult, ToolResultStatus,
    ToolTruncationReason,
};

pub fn check_background_command(
    context: &mut ToolContext,
    arguments: &ToolArguments,
) -> ToolExecutionResult {
    let spec = check_background_command_tool();
    (spec.handler)(context, arguments)
}

pub(crate) fn check_background_command_tool() -> ToolSpec {
    let mut spec = ToolSpec::new(
        "check_background_command",
        "Check status and output for a background command.",
        Arc::new(|context, arguments| {
            let session_id = string_arg(arguments.get("session_id"), "");
            let session_id = session_id.trim();
            if session_id.is_empty() {
                return tool_error_with_code("`session_id` is required", "session_id_required");
            }
            let payload = background_session_manager().check_for_tool(
                session_id,
                context.effective_workspace_backend(),
                &context.task_id,
                &context.tool_call_id,
            );
            if let Some(error) = payload.get("artifact_error").and_then(Value::as_str) {
                let error_code = payload
                    .get("artifact_error_code")
                    .and_then(Value::as_str)
                    .unwrap_or("artifact_persist_failed");
                return tool_error_with_code(
                    format!("failed to persist complete command output: {error}"),
                    error_code,
                );
            }
            match payload.get("status").and_then(Value::as_str) {
                Some("running") => tool_result_with_metadata(
                    ToolResultStatus::Running,
                    payload.clone(),
                    None,
                    ToolDirective::Continue,
                    background_metadata(&payload),
                ),
                Some("completed") => terminal_background_result(payload, true),
                Some("failed" | "timeout") => terminal_background_result(payload, false),
                _ => background_error(payload),
            }
        }),
    );
    if let Some(schema) = super::super::schemas::schema_for("check_background_command") {
        spec.schema = schema;
    }
    spec
}

fn terminal_background_result(payload: Value, success: bool) -> ToolExecutionResult {
    let mut content = payload
        .get("output")
        .and_then(Value::as_str)
        .unwrap_or_default()
        .to_string();
    if content.is_empty() && !success {
        content = "Background command failed".to_string();
    }
    let mut result = ToolExecutionResult::success("", content);
    result.status = if success {
        ToolResultStatus::Success
    } else {
        ToolResultStatus::Error
    };
    result.error_code = (!success).then(|| "background_command_failed".to_string());
    result.metadata = background_metadata(&payload);
    if payload.get("output_truncated").and_then(Value::as_bool) == Some(true) {
        let artifact = payload
            .get("artifact")
            .cloned()
            .and_then(|value| serde_json::from_value::<ToolArtifactRef>(value).ok());
        let Some(artifact) = artifact else {
            return tool_error_with_code(
                "complete background output has no recoverable artifact",
                "artifact_persist_failed",
            );
        };
        result.truncated = true;
        result.truncation_reason = Some(ToolTruncationReason::OutputLimit);
        result.original_bytes = payload.get("output_original_bytes").and_then(Value::as_u64);
        result.visible_bytes = payload.get("output_visible_bytes").and_then(Value::as_u64);
        result.artifact = Some(artifact);
    }
    result
}

fn background_metadata(payload: &Value) -> Metadata {
    let Some(object) = payload.as_object() else {
        return Metadata::new();
    };
    [
        "status",
        "session_id",
        "elapsed_seconds",
        "exit_code",
        "shell",
    ]
    .into_iter()
    .filter_map(|key| {
        let value = object.get(key)?;
        (!value.is_null()).then(|| (key.to_string(), value.clone()))
    })
    .collect()
}

fn background_error(payload: Value) -> ToolExecutionResult {
    let mut object = payload.as_object().cloned().unwrap_or_default();
    let status = object
        .get("status")
        .and_then(Value::as_str)
        .unwrap_or("missing")
        .to_string();
    let error = object
        .remove("error")
        .and_then(|value| value.as_str().map(str::to_string))
        .unwrap_or_else(|| match status.as_str() {
            "timeout" => "Background command timed out".to_string(),
            _ => "Background command failed".to_string(),
        });
    object.insert("ok".to_string(), Value::Bool(false));
    object.insert("error".to_string(), Value::String(error));
    object.insert(
        "error_code".to_string(),
        Value::String("background_command_failed".to_string()),
    );
    let content = Value::Object(object);
    let metadata = background_metadata(&content);
    tool_result_with_metadata(
        ToolResultStatus::Error,
        content,
        Some("background_command_failed"),
        ToolDirective::Continue,
        metadata,
    )
}