vv-agent 0.6.2

VectorVein agent runtime, SDK, CLI, tools, and workspace backends
Documentation
use std::collections::BTreeMap;

use serde_json::Value;

use crate::llm::LlmClient;
use std::path::Path;

use crate::types::{AgentResult, AgentTask, CycleRecord, ToolCall, ToolExecutionResult};

use super::{AgentRuntime, RuntimeRunControls};

impl<C: LlmClient> AgentRuntime<C> {
    pub(super) fn emit_log(
        &self,
        controls: &RuntimeRunControls,
        event: &str,
        payload: BTreeMap<String, Value>,
    ) {
        if let Some(handler) = &self.log_handler {
            if let Ok(mut handler) = handler.lock() {
                (handler)(event, &payload);
            }
        }
        if let Some(handler) = &controls.log_handler {
            handler(event, &payload);
        }
    }

    pub(super) fn emit_cycle_llm_response(
        &self,
        controls: &RuntimeRunControls,
        cycle: &CycleRecord,
    ) {
        self.emit_log(
            controls,
            "cycle_llm_response",
            BTreeMap::from([
                ("cycle".to_string(), Value::from(cycle.index)),
                (
                    "assistant_message".to_string(),
                    Value::String(cycle.assistant_message.clone()),
                ),
                (
                    "assistant_preview".to_string(),
                    Value::String(self.preview_text(&cycle.assistant_message)),
                ),
                (
                    "tool_calls".to_string(),
                    serde_json::to_value(&cycle.tool_calls).unwrap_or(Value::Null),
                ),
                (
                    "tool_call_names".to_string(),
                    Value::Array(
                        cycle
                            .tool_calls
                            .iter()
                            .map(|call| Value::String(call.name.clone()))
                            .collect(),
                    ),
                ),
                (
                    "tool_call_count".to_string(),
                    Value::from(cycle.tool_calls.len()),
                ),
                (
                    "memory_compacted".to_string(),
                    Value::Bool(cycle.memory_compacted),
                ),
                (
                    "token_usage".to_string(),
                    serde_json::to_value(&cycle.token_usage).unwrap_or(Value::Null),
                ),
            ]),
        );
    }

    pub(super) fn emit_run_started(
        &self,
        controls: &RuntimeRunControls,
        task: &AgentTask,
        workspace_path: &Path,
    ) {
        self.emit_log(
            controls,
            "run_started",
            BTreeMap::from([
                ("task_id".to_string(), Value::String(task.task_id.clone())),
                (
                    "agent_name".to_string(),
                    Value::String(
                        task.metadata
                            .get("agent_name")
                            .and_then(Value::as_str)
                            .unwrap_or(&task.task_id)
                            .to_string(),
                    ),
                ),
                ("model".to_string(), Value::String(task.model.clone())),
                (
                    "workspace".to_string(),
                    Value::String(workspace_path.display().to_string()),
                ),
                ("max_cycles".to_string(), Value::from(task.max_cycles)),
            ]),
        );
    }

    pub(super) fn emit_run_max_cycles(&self, controls: &RuntimeRunControls, result: &AgentResult) {
        self.emit_log(
            controls,
            "run_max_cycles",
            BTreeMap::from([
                ("cycle".to_string(), Value::from(result.cycles.len())),
                (
                    "final_answer".to_string(),
                    Value::String(
                        self.preview_text(&result.final_answer.clone().unwrap_or_default()),
                    ),
                ),
                (
                    "error".to_string(),
                    Value::String(self.preview_text(&result.error.clone().unwrap_or_default())),
                ),
            ]),
        );
    }

    pub(super) fn emit_tool_result(
        &self,
        controls: &RuntimeRunControls,
        cycle_index: u32,
        call: &ToolCall,
        result: &ToolExecutionResult,
    ) {
        self.emit_log(
            controls,
            "tool_result",
            BTreeMap::from([
                ("cycle".to_string(), Value::from(cycle_index)),
                ("tool_name".to_string(), Value::String(call.name.clone())),
                (
                    "tool_arguments".to_string(),
                    Value::Object(call.arguments.clone().into_iter().collect()),
                ),
                (
                    "tool_call_id".to_string(),
                    Value::String(result.tool_call_id.clone()),
                ),
                (
                    "status".to_string(),
                    serde_json::to_value(result.status).unwrap_or(Value::Null),
                ),
                (
                    "directive".to_string(),
                    serde_json::to_value(result.directive).unwrap_or(Value::Null),
                ),
                (
                    "error_code".to_string(),
                    result
                        .error_code
                        .clone()
                        .map(Value::String)
                        .unwrap_or(Value::Null),
                ),
                ("content".to_string(), Value::String(result.content.clone())),
                (
                    "content_preview".to_string(),
                    Value::String(self.preview_text(&result.content)),
                ),
                (
                    "metadata".to_string(),
                    Value::Object(result.metadata.clone().into_iter().collect()),
                ),
            ]),
        );
    }

    pub(super) fn preview_text(&self, text: &str) -> String {
        let cleaned = text.split_whitespace().collect::<Vec<_>>().join(" ");
        let Some(limit) = self.log_preview_chars.map(|limit| limit.max(40)) else {
            return cleaned;
        };
        if cleaned.chars().count() <= limit {
            return cleaned;
        }
        let prefix = cleaned
            .chars()
            .take(limit.saturating_sub(3))
            .collect::<String>();
        format!("{prefix}...")
    }
}