systemprompt-agent 0.1.22

Core Agent protocol module for systemprompt.io
Documentation
use anyhow::Result;
use std::sync::Arc;
use tokio::sync::mpsc;

use crate::models::AgentRuntimeInfo;
use crate::models::a2a::Artifact;
use crate::services::SkillService;
use crate::services::a2a_server::processing::artifact::ArtifactBuilder;
use crate::services::a2a_server::processing::message::StreamEvent;
use systemprompt_models::{AiMessage, AiProvider, RequestContext};

pub fn build_artifacts_from_results(
    tool_results: &[systemprompt_models::CallToolResult],
    tool_calls: &[systemprompt_models::ToolCall],
    tools: &[systemprompt_models::McpTool],
    context_id_str: &str,
    task_id_str: &str,
) -> Result<Vec<Artifact>> {
    if tool_results.is_empty() {
        tracing::info!("No tool_results - no artifacts expected");
        return Ok(Vec::new());
    }

    let has_structured_content = tool_results.iter().any(|r| r.structured_content.is_some());

    if !has_structured_content {
        tracing::info!(
            "No structured_content - ephemeral tool calls, skipping A2A artifact building"
        );
        return Ok(Vec::new());
    }

    tracing::info!(
        "Tool results contain structured_content - building A2A artifacts from agentic MCP calls"
    );

    let artifact_builder = ArtifactBuilder::new(
        tool_calls.to_vec(),
        tool_results.to_vec(),
        tools.to_vec(),
        context_id_str.to_string(),
        task_id_str.to_string(),
    );

    artifact_builder.build_artifacts()
}

pub struct SynthesizeFinalResponseParams<'a> {
    pub tool_calls: &'a [systemprompt_models::ToolCall],
    pub tool_results: &'a [systemprompt_models::CallToolResult],
    pub artifacts: &'a [Artifact],
    pub accumulated_text: &'a str,
    pub ai_service: Arc<dyn AiProvider>,
    pub agent_runtime: &'a AgentRuntimeInfo,
    pub ai_messages_for_synthesis: Vec<AiMessage>,
    pub tx: mpsc::UnboundedSender<StreamEvent>,
    pub request_ctx: RequestContext,
    pub skill_service: Arc<SkillService>,
}

pub async fn synthesize_final_response(params: SynthesizeFinalResponseParams<'_>) -> String {
    use crate::services::a2a_server::processing::ai_executor::{
        SynthesizeToolResultsParams, synthesize_tool_results_with_artifacts,
    };

    let SynthesizeFinalResponseParams {
        tool_calls,
        tool_results,
        artifacts,
        accumulated_text,
        ai_service,
        agent_runtime,
        ai_messages_for_synthesis,
        tx,
        request_ctx,
        skill_service,
    } = params;

    if !tool_calls.is_empty() && !tool_results.is_empty() {
        tracing::info!(
            tool_call_count = tool_calls.len(),
            artifact_count = artifacts.len(),
            "Synthesizing results from tool calls"
        );

        synthesize_tool_results_with_artifacts(SynthesizeToolResultsParams {
            ai_service,
            agent_runtime,
            original_messages: ai_messages_for_synthesis,
            initial_response: accumulated_text,
            tool_calls,
            tool_results,
            artifacts,
            tx,
            request_context: request_ctx,
            skill_service,
        })
        .await
        .unwrap_or_else(|()| {
            tracing::warn!("Synthesis failed, using initial response");
            accumulated_text.to_string()
        })
    } else {
        if tool_calls.is_empty() && !accumulated_text.is_empty() {
            tracing::warn!(
                response_len = accumulated_text.len(),
                "Synthesis skipped: Agent produced text without tool calls"
            );
        }
        accumulated_text.to_string()
    }
}