saya-agent 0.3.1

Agentic LLM loop and OpenAI-compatible provider clients for SAYA CLI.
Documentation
use super::{AgentError, check_cancelled, emit};
use crate::{
    AgentEvent, AgentEventSink, CancellationToken, ChatMessage, ChatProvider, ChatRequest,
    ProviderError, ProviderEvent, TokenUsage, ToolDefinition,
};
use futures_util::StreamExt;

/// Streams one provider turn: assembles the assistant message and reports
/// the token usage the provider disclosed for it.
pub(super) async fn receive(
    provider: &dyn ChatProvider,
    model: &str,
    messages: &[ChatMessage],
    definitions: &[ToolDefinition],
    sink: &dyn AgentEventSink,
    cancellation: &CancellationToken,
    events: &mut Vec<AgentEvent>,
) -> Result<(ChatMessage, TokenUsage), AgentError> {
    let mut stream = provider
        .stream(
            ChatRequest {
                model: model.into(),
                messages: messages.into(),
                tools: definitions.into(),
            },
            cancellation.clone(),
        )
        .await?;
    let (mut content, mut calls, mut complete) = (String::new(), Vec::new(), false);
    let mut usage = TokenUsage::default();
    while let Some(event) = stream.next().await {
        check_cancelled(cancellation)?;
        match event? {
            ProviderEvent::TextDelta(text) => {
                if content.len().saturating_add(text.len()) > crate::MAX_STREAM_BYTES {
                    return Err(AgentError::Provider(ProviderError::Request(
                        "provider stream exceeded size limit".into(),
                    )));
                }
                content.push_str(&text);
                emit(events, sink, AgentEvent::AssistantText { text }).await;
            }
            ProviderEvent::ToolCalls(value) => calls.extend(value),
            ProviderEvent::Usage(counts) => usage = counts,
            ProviderEvent::Done => complete = true,
        }
    }
    if !complete || (content.trim().is_empty() && calls.is_empty()) {
        return Err(AgentError::Provider(ProviderError::InvalidResponse));
    }
    Ok((
        ChatMessage {
            role: "assistant".into(),
            content,
            tool_calls: calls,
            tool_call_id: None,
        },
        usage,
    ))
}