vv-agent 0.7.1

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

use crate::config::build_vv_llm_from_local_settings;
use crate::model::ModelRef;
use crate::tools::common::trim_portable_whitespace;
use crate::types::{AgentTask, SubAgentConfig};

use super::super::types::{ResolvedSubAgentClient, SubTaskRunContext};

pub(super) fn resolve_sub_agent_client(
    context: &SubTaskRunContext,
    parent_task: &AgentTask,
    sub_agent: &SubAgentConfig,
) -> Result<ResolvedSubAgentClient, String> {
    let requested_model = trim_portable_whitespace(&sub_agent.model).to_string();
    let requested_backend = sub_agent
        .backend
        .as_deref()
        .map(trim_portable_whitespace)
        .filter(|backend| !backend.is_empty());
    let run_model_ref = requested_backend
        .map(|backend| ModelRef::backend(backend, &requested_model))
        .unwrap_or_else(|| ModelRef::named(&requested_model));

    if let Some(provider) = &context.model_provider {
        let resolved = provider
            .resolve(&run_model_ref)
            .map_err(|error| error.to_string())?;
        let client = provider
            .client(&resolved)
            .map_err(|error| error.to_string())?;
        let payload = resolved_payload(&resolved);
        return Ok(ResolvedSubAgentClient {
            llm_client: client,
            model_id: resolved.model_id,
            run_model_ref,
            native_multimodal: resolved.native_multimodal,
            context_length: resolved.context_length,
            max_output_tokens: resolved.max_output_tokens,
            payload,
        });
    }

    if let Some(settings_file) = &context.settings_file {
        let backend = requested_backend
            .map(str::to_string)
            .or_else(|| {
                context
                    .default_backend
                    .as_deref()
                    .map(trim_portable_whitespace)
                    .filter(|backend| !backend.is_empty())
                    .map(str::to_string)
            })
            .unwrap_or_else(|| "inline".to_string());
        let (client, resolved) = build_vv_llm_from_local_settings(
            settings_file,
            &backend,
            &requested_model,
            context.sub_agent_timeout_seconds,
        )
        .map_err(|error| error.to_string())?;
        let resolved_payload = resolved_payload(&resolved);
        return Ok(ResolvedSubAgentClient {
            llm_client: Arc::new(client),
            model_id: resolved.model_id,
            run_model_ref,
            native_multimodal: resolved.native_multimodal,
            context_length: resolved.context_length,
            max_output_tokens: resolved.max_output_tokens,
            payload: resolved_payload,
        });
    }

    if requested_backend.is_some() {
        return Err(
            "Sub-agent model resolution requires model_provider or settings_file when backend is explicitly configured."
                .to_string(),
        );
    }

    if requested_model != parent_task.model {
        return Err(
            "Sub-agent model resolution requires runtime settings_file when sub-agent model differs from parent model."
                .to_string(),
        );
    }

    Ok(ResolvedSubAgentClient {
        llm_client: context.llm_client.clone(),
        model_id: parent_task.model.clone(),
        run_model_ref,
        native_multimodal: parent_task.native_multimodal,
        context_length: parent_task
            .metadata
            .get("model_context_window")
            .and_then(serde_json::Value::as_u64),
        max_output_tokens: parent_task
            .metadata
            .get("reserved_output_tokens")
            .and_then(serde_json::Value::as_u64),
        payload: BTreeMap::new(),
    })
}

fn resolved_payload(resolved: &crate::config::ResolvedModelConfig) -> BTreeMap<String, String> {
    let mut payload = BTreeMap::from([
        ("backend".to_string(), resolved.backend.clone()),
        (
            "selected_model".to_string(),
            resolved.selected_model.clone(),
        ),
        ("model_id".to_string(), resolved.model_id.clone()),
    ]);
    if let Some(endpoint) = resolved.endpoint() {
        payload.insert("endpoint".to_string(), endpoint.endpoint_id.clone());
    }
    payload
}

#[cfg(test)]
mod tests {
    use std::sync::Arc;

    use serde_json::json;

    use super::resolve_sub_agent_client;
    use crate::llm::ScriptedLlmClient;
    use crate::runtime::sub_agents::types::SubTaskRunContext;
    use crate::runtime::sub_task_manager::SubTaskManager;
    use crate::tools::build_default_registry;
    use crate::types::{AgentTask, SubAgentConfig};
    use crate::workspace::MemoryWorkspaceBackend;

    #[test]
    fn portable_blank_backend_uses_default_backend_for_settings_resolution() {
        let contract: serde_json::Value = serde_json::from_str(include_str!(
            "../../../../tests/fixtures/parity/configured_sub_agent_v1.json"
        ))
        .expect("configured sub-agent contract");
        let workspace = tempfile::tempdir().expect("workspace");
        let settings_file = workspace.path().join("settings.json");
        std::fs::write(
            &settings_file,
            json!({
                "VERSION": "2",
                "endpoints": [{
                    "id": "child-endpoint",
                    "api_base": "https://example.invalid/v1",
                    "api_key": "sk-test"
                }],
                "backends": {
                    "moonshot": {
                        "models": {
                            "child-model": {
                                "id": "child-model",
                                "endpoints": [{
                                    "endpoint_id": "child-endpoint",
                                    "model_id": "child-model"
                                }]
                            }
                        }
                    }
                },
                "embedding_backends": {},
                "rerank_backends": {}
            })
            .to_string(),
        )
        .expect("write settings");
        let parent_task =
            AgentTask::new("parent-task", "parent-model", "Parent prompt", "Delegate");
        let mut sub_agent = SubAgentConfig::new("child-model", "Research");
        sub_agent.backend = Some(
            contract["validation"]["portable_whitespace"]["blank_backend_input"]
                .as_str()
                .expect("portable blank backend")
                .to_string(),
        );
        let context = SubTaskRunContext {
            llm_client: Arc::new(ScriptedLlmClient::new(Vec::new())),
            tool_registry: build_default_registry(),
            workspace_backend: Arc::new(MemoryWorkspaceBackend::default()),
            workspace_path: workspace.path().to_path_buf(),
            parent_task: parent_task.clone(),
            parent_shared_state: Default::default(),
            sub_task_manager: SubTaskManager::default(),
            parent_cancellation_token: None,
            settings_file: Some(settings_file),
            default_backend: Some("moonshot".to_string()),
            sub_agent_timeout_seconds: 30.0,
            stream_callback: None,
            parent_log_handler: None,
            parent_event_handler: None,
            parent_execution_context: None,
            model_provider: None,
            parent_run_context: None,
            tool_policy: None,
            budget_limits: None,
        };

        let resolved = resolve_sub_agent_client(&context, &parent_task, &sub_agent)
            .expect("resolve child model through default backend");

        assert_eq!(resolved.model_id, "child-model");
        assert_eq!(resolved.payload["backend"], "moonshot");
        assert_eq!(resolved.payload["endpoint"], "child-endpoint");
        assert_eq!(
            contract["model_resolution"]["blank_backend_treated_as_absent"],
            true
        );
    }
}