layer0 0.4.0

Protocol traits for composable agentic AI systems
Documentation
//! Effect system — side-effects declared by operators for external execution.

use crate::id::*;
use serde::{Deserialize, Serialize};

/// A side-effect declared by an operator. NOT executed by the operator —
/// the calling layer decides when and how to execute it.
///
/// This is the key composability mechanism. An operator running in-process
/// has its effects executed by a simple loop. An operator running in Temporal
/// has its effects serialized into the workflow history. An operator running
/// in a test harness has its effects captured for assertions.
///
/// The Custom variant ensures future effect types can be represented
/// without changing the enum. When a new effect type stabilizes
/// (used by 3+ implementations), it graduates to a named variant.
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum Effect {
    /// Write a value to persistent state.
    WriteMemory {
        /// The scope to write into.
        scope: Scope,
        /// The key to write.
        key: String,
        /// The value to store.
        value: serde_json::Value,
    },

    /// Delete a value from persistent state.
    DeleteMemory {
        /// The scope to delete from.
        scope: Scope,
        /// The key to delete.
        key: String,
    },

    /// Send a fire-and-forget signal to another agent or workflow.
    Signal {
        /// The target workflow to signal.
        target: WorkflowId,
        /// The signal payload.
        payload: SignalPayload,
    },

    /// Request that the orchestrator dispatch another agent.
    /// This is how delegation works — the operator doesn't call the
    /// other agent directly, it asks the orchestrator to do it.
    Delegate {
        /// The agent to delegate to.
        agent: AgentId,
        /// The input to send to the delegated agent.
        input: Box<OperatorInput>,
    },

    /// Hand off the conversation to another agent. Unlike Delegate,
    /// the current operator is done — the next agent takes over.
    Handoff {
        /// The agent to hand off to.
        agent: AgentId,
        /// State to pass to the next agent. This is NOT the full
        /// conversation — it's whatever the current agent thinks
        /// the next agent needs to continue.
        state: serde_json::Value,
    },

    /// Emit a log/trace event. Observers and telemetry consume these.
    Log {
        /// Severity level.
        level: LogLevel,
        /// Log message.
        message: String,
        /// Optional structured data.
        data: Option<serde_json::Value>,
    },

    /// Future effect types. Named string + arbitrary payload.
    /// Use this for domain-specific effects that aren't general
    /// enough for a named variant.
    Custom {
        /// The custom effect type identifier.
        effect_type: String,
        /// Arbitrary payload.
        data: serde_json::Value,
    },
}

// Forward-declare OperatorInput usage for the Delegate variant.
use crate::operator::OperatorInput;

/// Where state lives. Scopes are hierarchical — a session scope
/// is narrower than a workflow scope, which is narrower than global.
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "snake_case")]
pub enum Scope {
    /// Per-conversation.
    Session(SessionId),
    /// Per-workflow-execution.
    Workflow(WorkflowId),
    /// Per-agent within a workflow.
    Agent {
        /// The workflow this agent belongs to.
        workflow: WorkflowId,
        /// The agent within the workflow.
        agent: AgentId,
    },
    /// Shared across all workflows.
    Global,
    /// Future scopes.
    Custom(String),
}

/// Payload for inter-agent/workflow signals.
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignalPayload {
    /// The type of signal being sent.
    pub signal_type: String,
    /// Signal data.
    pub data: serde_json::Value,
}

impl SignalPayload {
    /// Create a new signal payload.
    pub fn new(signal_type: impl Into<String>, data: serde_json::Value) -> Self {
        Self {
            signal_type: signal_type.into(),
            data,
        }
    }
}

/// Log severity levels.
#[non_exhaustive]
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "snake_case")]
pub enum LogLevel {
    /// Finest-grained tracing.
    Trace,
    /// Debug-level detail.
    Debug,
    /// Informational messages.
    Info,
    /// Warnings.
    Warn,
    /// Errors.
    Error,
}