pidgin-lang 0.1.6

A compact agent handoff protocol runtime — parse, validate, resolve, and expand Pidgin packets
Documentation
use serde::Serialize;

use crate::ast::{FieldValue, PgnPacket};
use crate::registry::WorkflowRegistry;
use crate::resolver::ResolvedRef;
use crate::safety::SafetyResult;

#[derive(Debug, Serialize)]
pub struct ExpandedRef {
    pub reference: String,
    pub status: String,
    pub confidence: f32,
    pub path: Option<String>,
}

impl From<&ResolvedRef> for ExpandedRef {
    fn from(r: &ResolvedRef) -> Self {
        ExpandedRef {
            reference: r.original.clone(),
            status: format!("{:?}", r.status),
            confidence: r.confidence,
            path: r.resolved_path.as_ref().map(|p| p.display().to_string()),
        }
    }
}

#[derive(Debug, Serialize)]
pub struct ExpandedRunPacket {
    pub spec_version: String,
    pub run_id: String,
    pub workflow: String,
    pub mode: String,
    pub inputs: Vec<ExpandedRef>,
    pub outputs: Vec<ExpandedRef>,
    pub do_actions: Vec<String>,
    pub deny_actions: Vec<String>,
    pub effective_risk: String,
    pub human_required: bool,
    pub recommended_executor: String,
    pub fallback_executor: String,
    pub ttl: String,
    pub note: Option<String>,
}

#[derive(Debug, Serialize)]
pub struct ExpandedApprovalPacket {
    pub spec_version: String,
    pub run_id: String,
    pub workflow: String,
    pub risk: String,
    pub human_required: bool,
    pub actions: Vec<String>,
}

pub fn expand_to_run_packet(
    packet: &PgnPacket,
    resolved_refs: &[ResolvedRef],
    safety: &SafetyResult,
    workflows: &WorkflowRegistry,
) -> ExpandedRunPacket {
    let wf_name = packet
        .fields
        .get("wf")
        .and_then(|v| match v {
            FieldValue::Scalar(s) => Some(s.as_str()),
            _ => None,
        })
        .unwrap_or("unknown");

    let workflow_entry = workflows.workflows.get(wf_name);

    let inputs: Vec<ExpandedRef> = resolved_refs
        .iter()
        .filter(|r| matches!(packet.fields.get("in"), Some(FieldValue::List(refs)) if refs.contains(&r.original)))
        .map(ExpandedRef::from)
        .collect();

    let outputs: Vec<ExpandedRef> = resolved_refs
        .iter()
        .filter(|r| matches!(packet.fields.get("out"), Some(FieldValue::List(refs)) if refs.contains(&r.original)))
        .map(ExpandedRef::from)
        .collect();

    let do_actions = packet
        .fields
        .get("do")
        .and_then(|v| match v {
            FieldValue::List(items) => Some(items.clone()),
            _ => None,
        })
        .unwrap_or_default();

    let deny_actions = packet
        .fields
        .get("deny")
        .and_then(|v| match v {
            FieldValue::List(items) => Some(items.clone()),
            _ => None,
        })
        .unwrap_or_default();

    let note = packet
        .fields
        .get("note")
        .and_then(|v| match v {
            FieldValue::Scalar(s) => Some(s.clone()),
            _ => None,
        });

    let recommended_executor = workflow_entry
        .map(|w| w.recommended_executor.clone())
        .unwrap_or_else(|| "unknown".to_string());

    let fallback_executor = workflow_entry
        .map(|w| w.fallback_executor.clone())
        .unwrap_or_else(|| "unknown".to_string());

    let ttl = packet
        .fields
        .get("ttl")
        .and_then(|v| match v {
            FieldValue::Scalar(s) => Some(s.clone()),
            _ => None,
        })
        .unwrap_or_else(|| "24h".to_string());

    ExpandedRunPacket {
        spec_version: "1.0".to_string(),
        run_id: packet.run_id.clone(),
        workflow: wf_name.to_string(),
        mode: packet
            .fields
            .get("mode")
            .and_then(|v| match v {
                FieldValue::Scalar(s) => Some(s.clone()),
                _ => None,
            })
            .unwrap_or_else(|| "unknown".to_string()),
        inputs,
        outputs,
        do_actions,
        deny_actions,
        effective_risk: safety.effective_risk.clone(),
        human_required: safety.human_required,
        recommended_executor,
        fallback_executor,
        ttl,
        note,
    }
}

pub fn expand_to_approval_request(
    packet: &PgnPacket,
    safety: &SafetyResult,
    _workflows: &WorkflowRegistry,
) -> ExpandedApprovalPacket {
    let wf_name = packet
        .fields
        .get("wf")
        .and_then(|v| match v {
            FieldValue::Scalar(s) => Some(s.as_str()),
            _ => None,
        })
        .unwrap_or("unknown");

    let actions = packet
        .fields
        .get("do")
        .and_then(|v| match v {
            FieldValue::List(items) => Some(items.clone()),
            _ => None,
        })
        .unwrap_or_default();

    ExpandedApprovalPacket {
        spec_version: "1.0".to_string(),
        run_id: packet.run_id.clone(),
        workflow: wf_name.to_string(),
        risk: safety.effective_risk.clone(),
        human_required: safety.human_required,
        actions,
    }
}