vtcode 0.99.1

A Rust-based terminal coding agent with modular architecture supporting multiple LLM providers
use crate::agent::runloop::unified::inline_events::harness::HarnessEventEmitter;
use serde_json::Value;
use vtcode_core::core::agent::events::{
    ToolOutputPayload, error_item_completed_event, tool_invocation_completed_event,
    tool_output_completed_event, tool_output_payload_from_value,
};
use vtcode_core::exec::events::ToolCallStatus;

use super::status::ToolExecutionStatus;

pub(super) fn emit_tool_completion_status(
    harness_emitter: Option<&HarnessEventEmitter>,
    tool_started_emitted: bool,
    tool_execution_started: bool,
    tool_item_id: &str,
    tool_call_id: &str,
    tool_name: &str,
    args: &Value,
    status: ToolCallStatus,
    exit_code: Option<i32>,
    spool_path: Option<&str>,
    aggregated_output: impl Into<String>,
) {
    if !tool_started_emitted {
        return;
    }

    if let Some(emitter) = harness_emitter {
        let aggregated_output = aggregated_output.into();
        let _ = emitter.emit(tool_invocation_completed_event(
            tool_item_id.to_string(),
            tool_name,
            Some(args),
            Some(tool_call_id),
            status.clone(),
        ));
        if tool_execution_started {
            let _ = emitter.emit(tool_output_completed_event(
                tool_item_id.to_string(),
                Some(tool_call_id),
                status,
                exit_code,
                spool_path,
                aggregated_output,
            ));
        } else if !aggregated_output.is_empty() {
            let _ = emitter.emit(error_item_completed_event(
                format!("{tool_item_id}:error"),
                aggregated_output,
            ));
        }
    }
}

pub(super) fn emit_tool_completion_for_status(
    harness_emitter: Option<&HarnessEventEmitter>,
    tool_started_emitted: bool,
    tool_execution_started: bool,
    tool_item_id: &str,
    tool_call_id: &str,
    tool_name: &str,
    args: &Value,
    tool_status: &ToolExecutionStatus,
) {
    let (status, exit_code, output_payload) = match tool_status {
        ToolExecutionStatus::Success {
            output,
            command_success,
            ..
        } => (
            if *command_success {
                ToolCallStatus::Completed
            } else {
                ToolCallStatus::Failed
            },
            output
                .get("exit_code")
                .and_then(Value::as_i64)
                .and_then(|code| i32::try_from(code).ok()),
            tool_output_payload_from_value(output),
        ),
        ToolExecutionStatus::Failure { error } => (
            ToolCallStatus::Failed,
            None,
            ToolOutputPayload {
                aggregated_output: error.user_message(),
                spool_path: None,
            },
        ),
        ToolExecutionStatus::Timeout { error } => (
            ToolCallStatus::Failed,
            None,
            ToolOutputPayload {
                aggregated_output: error.user_message(),
                spool_path: None,
            },
        ),
        ToolExecutionStatus::Cancelled => (
            ToolCallStatus::Failed,
            None,
            ToolOutputPayload {
                aggregated_output: "Tool execution cancelled".to_string(),
                spool_path: None,
            },
        ),
    };
    emit_tool_completion_status(
        harness_emitter,
        tool_started_emitted,
        tool_execution_started,
        tool_item_id,
        tool_call_id,
        tool_name,
        args,
        status,
        exit_code,
        output_payload.spool_path.as_deref(),
        output_payload.aggregated_output,
    );
}

#[cfg(test)]
mod tests {
    use super::*;
    use serde_json::{Value, json};

    #[test]
    fn aggregates_command_output_without_duplicates() {
        let output = json!({
            "output": "same",
            "stdout": "same",
            "stderr": "warn"
        });

        let payload = tool_output_payload_from_value(&output);
        assert_eq!(payload.aggregated_output, "same\nwarn");
        assert_eq!(payload.spool_path, None);
    }

    #[test]
    fn includes_content_when_command_stream_fields_absent() {
        let output = json!({
            "content": "file body",
            "path": "README.md"
        });

        let payload = tool_output_payload_from_value(&output);
        assert_eq!(payload.aggregated_output, "file body");
        assert_eq!(payload.spool_path, None);
    }

    #[test]
    fn prefers_spool_reference_over_inline_output() {
        let output = json!({
            "output": "preview",
            "spool_path": ".vtcode/context/tool_outputs/run-1.txt"
        });

        let payload = tool_output_payload_from_value(&output);
        assert_eq!(payload.aggregated_output, "");
        assert_eq!(
            payload.spool_path.as_deref(),
            Some(".vtcode/context/tool_outputs/run-1.txt")
        );
    }

    #[test]
    fn structured_list_output_emits_compact_summary() {
        let output = json!({
            "items": [
                {"path": "vtcode-tui/src/app.rs", "type": "file"},
                {"path": "vtcode-tui/src/core_tui", "type": "directory"}
            ],
            "count": 2,
            "total": 11
        });

        let payload = tool_output_payload_from_value(&output);
        assert!(payload.aggregated_output.contains("Listed 11 items"));
        assert!(payload.aggregated_output.contains("vtcode-tui/src/app.rs"));
    }

    #[test]
    fn non_zero_command_marks_failed_completion() {
        let status = ToolExecutionStatus::Success {
            output: json!({
                "stdout": "boom",
                "exit_code": 1
            }),
            stdout: Some("boom".to_string()),
            modified_files: vec![],
            command_success: false,
        };

        let (event_status, exit_code, output_payload) = match &status {
            ToolExecutionStatus::Success {
                output,
                command_success,
                ..
            } => (
                if *command_success {
                    ToolCallStatus::Completed
                } else {
                    ToolCallStatus::Failed
                },
                output
                    .get("exit_code")
                    .and_then(Value::as_i64)
                    .and_then(|code| i32::try_from(code).ok()),
                tool_output_payload_from_value(output),
            ),
            _ => unreachable!("success status expected"),
        };

        assert_eq!(event_status, ToolCallStatus::Failed);
        assert_eq!(exit_code, Some(1));
        assert_eq!(output_payload.aggregated_output, "boom");
        assert_eq!(output_payload.spool_path, None);
    }
}