enact-core 0.0.1

Core agent runtime for Enact - Graph-Native AI agents
Documentation
//! Telemetry Spans - OpenTelemetry span helpers for execution tracing
//!
//! This module provides utilities for creating and managing OpenTelemetry spans
//! that correlate with ExecutionId and StepId for distributed tracing.
//!
//! ## Span Hierarchy
//! ```text
//! Execution Span (trace_id = TraceContext.trace_id)
//!   ├── Step Span (span_id = unique per step)
//!   │     └── Tool Span
//!   ├── Step Span
//!   │     └── LLM Span
//!   └── Child Execution Span (sub-agent)
//!         └── Step Span...
//! ```
//!
//! @see docs/TECHNICAL/01-EXECUTION-TELEMETRY.md

use crate::kernel::{ExecutionId, StepId, StepType};
use crate::runner::TraceContext;

/// Span attributes for execution-level spans
pub struct ExecutionSpanAttributes {
    /// Execution ID
    pub execution_id: String,
    /// Parent ID (if nested execution)
    pub parent_id: Option<String>,
    /// Parent type
    pub parent_type: Option<String>,
    /// Tenant ID
    pub tenant_id: Option<String>,
    /// User ID
    pub user_id: Option<String>,
}

impl ExecutionSpanAttributes {
    /// Create from an ExecutionId
    pub fn new(execution_id: &ExecutionId) -> Self {
        Self {
            execution_id: execution_id.as_str().to_string(),
            parent_id: None,
            parent_type: None,
            tenant_id: None,
            user_id: None,
        }
    }

    /// Add parent context
    pub fn with_parent(mut self, parent_id: impl Into<String>, parent_type: impl Into<String>) -> Self {
        self.parent_id = Some(parent_id.into());
        self.parent_type = Some(parent_type.into());
        self
    }

    /// Add tenant context
    pub fn with_tenant(mut self, tenant_id: impl Into<String>) -> Self {
        self.tenant_id = Some(tenant_id.into());
        self
    }

    /// Add user context
    pub fn with_user(mut self, user_id: impl Into<String>) -> Self {
        self.user_id = Some(user_id.into());
        self
    }

    /// Convert to a map of attributes for OpenTelemetry
    pub fn to_attributes(&self) -> Vec<(&'static str, String)> {
        let mut attrs = vec![("enact.execution_id", self.execution_id.clone())];

        if let Some(ref parent_id) = self.parent_id {
            attrs.push(("enact.parent_id", parent_id.clone()));
        }
        if let Some(ref parent_type) = self.parent_type {
            attrs.push(("enact.parent_type", parent_type.clone()));
        }
        if let Some(ref tenant_id) = self.tenant_id {
            attrs.push(("enact.tenant_id", tenant_id.clone()));
        }
        if let Some(ref user_id) = self.user_id {
            attrs.push(("enact.user_id", user_id.clone()));
        }

        attrs
    }
}

/// Span attributes for step-level spans
pub struct StepSpanAttributes {
    /// Execution ID
    pub execution_id: String,
    /// Step ID
    pub step_id: String,
    /// Step type
    pub step_type: String,
    /// Step name
    pub name: String,
}

impl StepSpanAttributes {
    /// Create from ExecutionId and StepId
    pub fn new(
        execution_id: &ExecutionId,
        step_id: &StepId,
        step_type: StepType,
        name: impl Into<String>,
    ) -> Self {
        Self {
            execution_id: execution_id.as_str().to_string(),
            step_id: step_id.as_str().to_string(),
            step_type: step_type.to_string(),
            name: name.into(),
        }
    }

    /// Convert to a map of attributes for OpenTelemetry
    pub fn to_attributes(&self) -> Vec<(&'static str, String)> {
        vec![
            ("enact.execution_id", self.execution_id.clone()),
            ("enact.step_id", self.step_id.clone()),
            ("enact.step_type", self.step_type.clone()),
            ("enact.step_name", self.name.clone()),
        ]
    }
}

/// Span attributes for tool calls
pub struct ToolSpanAttributes {
    /// Execution ID
    pub execution_id: String,
    /// Step ID
    pub step_id: String,
    /// Tool name
    pub tool_name: String,
}

impl ToolSpanAttributes {
    /// Create from ExecutionId, StepId, and tool name
    pub fn new(
        execution_id: &ExecutionId,
        step_id: &StepId,
        tool_name: impl Into<String>,
    ) -> Self {
        Self {
            execution_id: execution_id.as_str().to_string(),
            step_id: step_id.as_str().to_string(),
            tool_name: tool_name.into(),
        }
    }

    /// Convert to a map of attributes for OpenTelemetry
    pub fn to_attributes(&self) -> Vec<(&'static str, String)> {
        vec![
            ("enact.execution_id", self.execution_id.clone()),
            ("enact.step_id", self.step_id.clone()),
            ("enact.tool_name", self.tool_name.clone()),
        ]
    }
}

/// Span attributes for LLM calls
pub struct LlmSpanAttributes {
    /// Execution ID
    pub execution_id: String,
    /// Step ID
    pub step_id: String,
    /// Model provider
    pub provider: String,
    /// Model name
    pub model: String,
    /// Input tokens (optional)
    pub input_tokens: Option<u32>,
    /// Output tokens (optional)
    pub output_tokens: Option<u32>,
}

impl LlmSpanAttributes {
    /// Create from ExecutionId, StepId, provider, and model
    pub fn new(
        execution_id: &ExecutionId,
        step_id: &StepId,
        provider: impl Into<String>,
        model: impl Into<String>,
    ) -> Self {
        Self {
            execution_id: execution_id.as_str().to_string(),
            step_id: step_id.as_str().to_string(),
            provider: provider.into(),
            model: model.into(),
            input_tokens: None,
            output_tokens: None,
        }
    }

    /// Add token counts
    pub fn with_tokens(mut self, input: u32, output: u32) -> Self {
        self.input_tokens = Some(input);
        self.output_tokens = Some(output);
        self
    }

    /// Convert to a map of attributes for OpenTelemetry
    pub fn to_attributes(&self) -> Vec<(&'static str, String)> {
        let mut attrs = vec![
            ("enact.execution_id", self.execution_id.clone()),
            ("enact.step_id", self.step_id.clone()),
            ("gen_ai.system", self.provider.clone()),
            ("gen_ai.request.model", self.model.clone()),
        ];

        if let Some(tokens) = self.input_tokens {
            attrs.push(("gen_ai.usage.input_tokens", tokens.to_string()));
        }
        if let Some(tokens) = self.output_tokens {
            attrs.push(("gen_ai.usage.output_tokens", tokens.to_string()));
        }

        attrs
    }
}

/// Create span name for an execution
pub fn execution_span_name(execution_id: &ExecutionId) -> String {
    format!("execution:{}", execution_id.as_str())
}

/// Create span name for a step
pub fn step_span_name(step_type: &StepType, name: &str) -> String {
    format!("step:{}:{}", step_type, name)
}

/// Create span name for a tool call
pub fn tool_span_name(tool_name: &str) -> String {
    format!("tool:{}", tool_name)
}

/// Create span name for an LLM call
pub fn llm_span_name(provider: &str, model: &str) -> String {
    format!("llm:{}:{}", provider, model)
}

/// Extract trace context from a RuntimeContext trace
pub fn extract_trace_context(trace: &TraceContext) -> (String, String) {
    (trace.trace_id.clone(), trace.span_id.clone())
}