systemprompt-agent 0.2.2

Agent-to-Agent (A2A) protocol for systemprompt.io AI governance: streaming, JSON-RPC models, task lifecycle, .well-known discovery, and governed agent orchestration.
Documentation
use crate::models::a2a::{
    Artifact, DataPart, FileContent, FilePart, Message, MessageRole, Part, TextPart,
};
use crate::models::{
    ArtifactPartRow, ArtifactRow, ExecutionStepBatchRow, MessagePart, TaskMessage,
};
use std::collections::HashMap;
use systemprompt_identifiers::{ArtifactId, ContextId, MessageId, TaskId};
use systemprompt_models::a2a::ArtifactMetadata;
use systemprompt_models::{ExecutionStep, StepContent, StepId, StepStatus};

use super::converters;

pub fn build_execution_steps(
    steps: Option<&Vec<&ExecutionStepBatchRow>>,
) -> Option<Vec<ExecutionStep>> {
    let steps = steps?;
    if steps.is_empty() {
        return None;
    }

    let result: Vec<ExecutionStep> = steps
        .iter()
        .filter_map(|row| {
            let status = row
                .status
                .parse::<StepStatus>()
                .map_err(|e| {
                    tracing::debug!(step_id = %row.step_id, error = %e, "Invalid step status, skipping");
                    e
                })
                .ok()?;
            let content: StepContent = serde_json::from_value(row.content.clone())
                .map_err(|e| {
                    tracing::debug!(step_id = %row.step_id, error = %e, "Invalid step content, skipping");
                    e
                })
                .ok()?;

            Some(ExecutionStep {
                step_id: StepId(row.step_id.to_string()),
                task_id: row.task_id.clone(),
                status,
                started_at: row.started_at,
                completed_at: row.completed_at,
                duration_ms: row.duration_ms,
                error_message: row.error_message.clone(),
                content,
            })
        })
        .collect();

    if result.is_empty() {
        None
    } else {
        Some(result)
    }
}

pub fn build_messages(
    messages: Option<&Vec<&TaskMessage>>,
    parts_by_message: &HashMap<MessageId, Vec<&MessagePart>>,
) -> Option<Vec<Message>> {
    let messages = messages?;
    if messages.is_empty() {
        return None;
    }

    let mut result = Vec::new();
    for msg_row in messages {
        let parts = build_message_parts(parts_by_message.get(&msg_row.message_id));

        let reference_task_ids = msg_row
            .reference_task_ids
            .as_ref()
            .map(|ids| ids.iter().map(|id| TaskId::new(id.clone())).collect());

        let mut final_metadata = msg_row
            .metadata
            .clone()
            .unwrap_or_else(|| serde_json::json!({}));
        if let Some(client_id) = &msg_row.client_message_id {
            if let Some(obj) = final_metadata.as_object_mut() {
                obj.insert(
                    "clientMessageId".to_string(),
                    serde_json::Value::String(client_id.clone()),
                );
            }
        }

        let role = match msg_row.role.as_str() {
            "user" | "ROLE_USER" => MessageRole::User,
            _ => MessageRole::Agent,
        };

        result.push(Message {
            role,
            parts,
            message_id: msg_row.message_id.clone(),
            task_id: Some(msg_row.task_id.clone()),
            context_id: msg_row.context_id.clone().unwrap_or_else(ContextId::empty),
            metadata: if final_metadata == serde_json::json!({}) {
                None
            } else {
                Some(final_metadata)
            },
            extensions: None,
            reference_task_ids,
        });
    }

    Some(result)
}

fn build_message_parts(parts: Option<&Vec<&MessagePart>>) -> Vec<Part> {
    let Some(parts) = parts else {
        return Vec::new();
    };

    parts
        .iter()
        .filter_map(|p| converters::build_part_from_row(p))
        .collect()
}

pub fn build_artifacts(
    artifacts: Option<&Vec<&ArtifactRow>>,
    artifact_parts_by_id: &HashMap<ArtifactId, Vec<&ArtifactPartRow>>,
) -> Option<Vec<Artifact>> {
    let artifacts = artifacts?;
    if artifacts.is_empty() {
        return None;
    }

    let mut result = Vec::new();
    for row in artifacts {
        let artifact = build_artifact(row, artifact_parts_by_id);
        result.push(artifact);
    }

    Some(result)
}

fn build_artifact(
    row: &ArtifactRow,
    artifact_parts_by_id: &HashMap<ArtifactId, Vec<&ArtifactPartRow>>,
) -> Artifact {
    let metadata_value = row
        .metadata
        .clone()
        .unwrap_or_else(|| serde_json::json!({}));

    let metadata = ArtifactMetadata {
        artifact_type: row.artifact_type.clone(),
        context_id: row.context_id.clone().unwrap_or_else(ContextId::empty),
        created_at: row.created_at.to_rfc3339(),
        task_id: row.task_id.clone(),
        rendering_hints: metadata_value.get("rendering_hints").cloned(),
        source: row.source.clone(),
        mcp_execution_id: row.mcp_execution_id.clone().map(|id| id.to_string()),
        mcp_schema: metadata_value.get("mcp_schema").cloned(),
        is_internal: metadata_value
            .get("is_internal")
            .and_then(serde_json::Value::as_bool),
        fingerprint: row.fingerprint.clone(),
        tool_name: row.tool_name.clone(),
        execution_index: metadata_value
            .get("execution_index")
            .and_then(serde_json::Value::as_u64)
            .map(|v| v as usize),
        skill_id: row.skill_id.clone(),
        skill_name: row.skill_name.clone(),
    };

    let extensions = metadata_value
        .get("artifact_extensions")
        .and_then(|v| v.as_array())
        .cloned()
        .unwrap_or_else(|| {
            vec![serde_json::json!(
                "https://systemprompt.io/extensions/artifact-rendering/v1"
            )]
        });

    let parts = build_artifact_parts(artifact_parts_by_id.get(&row.artifact_id));

    Artifact {
        id: row.artifact_id.clone(),
        title: row.name.clone(),
        description: row.description.clone(),
        parts,
        extensions,
        metadata,
    }
}

fn build_artifact_parts(parts: Option<&Vec<&ArtifactPartRow>>) -> Vec<Part> {
    let Some(parts) = parts else {
        return Vec::new();
    };

    let mut result = Vec::new();
    for row in parts {
        let part = match row.part_kind.as_str() {
            "text" => {
                let text = row.text_content.clone().unwrap_or_else(String::new);
                Part::Text(TextPart { text })
            },
            "file" => Part::File(FilePart {
                file: FileContent {
                    name: row.file_name.clone(),
                    mime_type: row.file_mime_type.clone(),
                    bytes: row.file_bytes.clone(),
                    url: row.file_uri.clone(),
                },
            }),
            "data" => {
                let Some(data_value) = &row.data_content else {
                    continue;
                };
                let Some(data) = data_value.as_object() else {
                    continue;
                };
                Part::Data(DataPart { data: data.clone() })
            },
            _ => continue,
        };
        result.push(part);
    }

    result
}