saya-agent 0.1.2

Agentic LLM loop and OpenAI-compatible provider clients for SAYA CLI.
Documentation
use crate::{ProviderError, ToolCall};
use std::collections::BTreeMap;

#[derive(Default)]
pub(super) struct ToolAssembly {
    calls: BTreeMap<usize, PartialCall>,
}

#[derive(Default)]
struct PartialCall {
    id: Option<String>,
    name: String,
    arguments: String,
}

impl ToolAssembly {
    pub(super) fn push(
        &mut self,
        index: usize,
        id: Option<&str>,
        name: Option<&str>,
        arguments: Option<&str>,
    ) -> Result<(), ProviderError> {
        let call = self.calls.entry(index).or_default();
        if let Some(id) = id {
            if let Some(previous) = &call.id {
                if previous != id {
                    return Err(ProviderError::InvalidResponse);
                }
            } else {
                call.id = Some(id.into());
            }
        }
        if let Some(name) = name {
            call.name.push_str(name);
        }
        if let Some(arguments) = arguments {
            call.arguments.push_str(arguments);
        }
        Ok(())
    }

    pub(super) fn finish(self) -> Result<Vec<ToolCall>, ProviderError> {
        self.calls
            .into_iter()
            .map(|(index, call)| {
                let arguments: serde_json::Value = serde_json::from_str(&call.arguments)
                    .map_err(|_| ProviderError::InvalidResponse)?;
                if call.name.is_empty() || !arguments.is_object() {
                    return Err(ProviderError::InvalidResponse);
                }
                Ok(ToolCall {
                    id: call.id.unwrap_or_else(|| format!("call-{index}")),
                    name: call.name,
                    arguments,
                })
            })
            .collect()
    }

    pub(super) fn is_empty(&self) -> bool {
        self.calls.is_empty()
    }
}