use std::fmt::{Display, Formatter};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[serde(rename_all = "snake_case")]
pub enum NodeCapability {
Network,
Filesystem,
Database,
Credential,
Process,
}
impl NodeCapability {
pub const ALL: &'static [Self] = &[
Self::Network,
Self::Filesystem,
Self::Database,
Self::Credential,
Self::Process,
];
pub const fn as_str(self) -> &'static str {
match self {
Self::Network => "network",
Self::Filesystem => "filesystem",
Self::Database => "database",
Self::Credential => "credential",
Self::Process => "process",
}
}
}
impl Display for NodeCapability {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(rename_all = "snake_case")]
pub enum DslFlagKind {
#[default]
Scalar,
CommaSeparatedList,
RepeatedList,
Bool,
KeyValuePairs,
SchemaField,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct DslFlag {
pub flag: String,
pub config_key: String,
pub description: String,
pub kind: DslFlagKind,
pub required: bool,
}
pub fn engine_common_dsl_flags() -> Vec<DslFlag> {
vec![
DslFlag {
flag: "--title".to_string(),
config_key: "title".to_string(),
description: "Custom display title for this node instance. \
Overrides the default node kind title shown on the canvas."
.to_string(),
kind: DslFlagKind::Scalar,
required: false,
},
DslFlag {
flag: "--timeout".to_string(),
config_key: "timeout_secs".to_string(),
description: "Engine-level execution timeout for this node in seconds. \
Overrides the project-level pipeline_node_timeout_secs. \
Clamped to 5–3600s."
.to_string(),
kind: DslFlagKind::Scalar,
required: false,
},
DslFlag {
flag: "--preview".to_string(),
config_key: "preview.out".to_string(),
description: "Render this node's output under its box on the canvas: \
`<as>[:<path>]` where as is image|video|audio|pdf|json|text|table|html \
and path is a dot path into the payload. `off` removes it. \
Presentation only — never read by the engine."
.to_string(),
kind: DslFlagKind::Scalar,
required: false,
},
DslFlag {
flag: "--preview-in".to_string(),
config_key: "preview.in".to_string(),
description: "Render this node's input under its box on the canvas: \
`<as>[:<path>]`, same kinds and path rule as `--preview`. \
`off` removes it. Presentation only — never read by the engine."
.to_string(),
kind: DslFlagKind::Scalar,
required: false,
},
]
}
pub fn engine_common_fields() -> Vec<NodeFieldDef> {
vec![NodeFieldDef {
name: "timeout_secs".to_string(),
label: "Timeout (s)".to_string(),
field_type: NodeFieldType::Text,
help: Some(
"Engine-level execution timeout in seconds. Overrides project default. Range: 5–3600."
.to_string(),
),
placeholder: Some("30".to_string()),
..Default::default()
}]
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "snake_case")]
pub enum NodeFieldType {
Text,
Textarea,
CodeEditor,
Select,
Datalist,
MethodButtons,
CopyUrl,
Checkbox,
Section,
MultiCheckbox,
KeyValuePairs,
ClaimsPairs,
ParamsBuilder,
Number,
SecureRequestBindings,
MatchCases,
SourceBindings,
}
impl Default for NodeFieldType {
fn default() -> Self {
Self::Text
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NodeFieldDataSource {
CredentialsAll,
CredentialsPostgres,
CredentialsJwt,
TemplatesPages,
CredentialsBrowser,
CredentialsOpenAi,
CredentialsSecureRequest,
AiTools,
FunctionPipelines,
CredentialJwtRoles,
CredentialsHttpAuth,
CredentialsWebhookAuth,
CredentialsByKind(String),
}
impl serde::Serialize for NodeFieldDataSource {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
Self::CredentialsAll => serializer.serialize_str("credentials_all"),
Self::CredentialsPostgres => serializer.serialize_str("credentials_postgres"),
Self::CredentialsJwt => serializer.serialize_str("credentials_jwt"),
Self::TemplatesPages => serializer.serialize_str("templates_pages"),
Self::CredentialsBrowser => serializer.serialize_str("credentials_browser"),
Self::CredentialsOpenAi => serializer.serialize_str("credentials_open_ai"),
Self::CredentialsSecureRequest => {
serializer.serialize_str("credentials_secure_request")
}
Self::AiTools => serializer.serialize_str("ai_tools"),
Self::FunctionPipelines => serializer.serialize_str("function_pipelines"),
Self::CredentialJwtRoles => serializer.serialize_str("credential_jwt_roles"),
Self::CredentialsHttpAuth => serializer.serialize_str("credentials_http_auth"),
Self::CredentialsWebhookAuth => serializer.serialize_str("credentials_webhook_auth"),
Self::CredentialsByKind(kind) => {
serializer.serialize_str(&format!("credentials:{}", kind))
}
}
}
}
impl<'de> serde::Deserialize<'de> for NodeFieldDataSource {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
match s.as_str() {
"credentials_all" => Ok(Self::CredentialsAll),
"credentials_postgres" => Ok(Self::CredentialsPostgres),
"credentials_jwt" => Ok(Self::CredentialsJwt),
"templates_pages" => Ok(Self::TemplatesPages),
"credentials_browser" => Ok(Self::CredentialsBrowser),
"credentials_open_ai" => Ok(Self::CredentialsOpenAi),
"credentials_secure_request" => Ok(Self::CredentialsSecureRequest),
"ai_tools" => Ok(Self::AiTools),
"function_pipelines" => Ok(Self::FunctionPipelines),
"credential_jwt_roles" => Ok(Self::CredentialJwtRoles),
"credentials_http_auth" => Ok(Self::CredentialsHttpAuth),
"credentials_webhook_auth" => Ok(Self::CredentialsWebhookAuth),
other => {
if let Some(kind) = other.strip_prefix("credentials:") {
Ok(Self::CredentialsByKind(kind.to_string()))
} else {
Err(serde::de::Error::unknown_variant(
other,
&[
"credentials_all",
"credentials_postgres",
"credentials_jwt",
"templates_pages",
"credentials_browser",
"credentials_open_ai",
"credentials_secure_request",
"ai_tools",
"function_pipelines",
"credential_jwt_roles",
"credentials_http_auth",
"credentials_webhook_auth",
"credentials:<kind>",
],
))
}
}
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct SelectOptionDef {
pub value: String,
pub label: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct SidebarItem {
pub label: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub type_hint: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct SidebarSection {
pub title: String,
pub items: Vec<SidebarItem>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct NodeFieldDef {
pub name: String,
pub label: String,
#[serde(rename = "type", default)]
pub field_type: NodeFieldType,
#[serde(skip_serializing_if = "Option::is_none")]
pub help: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub placeholder: Option<String>,
#[serde(default)]
pub readonly: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub rows: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub language: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub options: Vec<SelectOptionDef>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data_source: Option<NodeFieldDataSource>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default_value: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub sidebar: Vec<SidebarSection>,
#[serde(skip_serializing_if = "Option::is_none")]
pub span: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(untagged)]
pub enum LayoutItem {
Field(String),
Row { row: Vec<LayoutItem> },
Col { col: Vec<LayoutItem> },
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PipelineGraph {
pub id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub metadata: Option<PipelineGraphMetadata>,
#[serde(default)]
pub entry_nodes: Vec<String>,
pub nodes: Vec<PipelineNode>,
pub edges: Vec<PipelineEdge>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub notes: Vec<PipelineNote>,
}
impl PipelineGraph {
pub fn entry_node_ids(&self) -> Vec<&str> {
if !self.entry_nodes.is_empty() {
return self.entry_nodes.iter().map(String::as_str).collect();
}
let targets: std::collections::HashSet<&str> = self
.edges
.iter()
.map(|edge| edge.to_node.as_str())
.collect();
let roots: Vec<&str> = self
.nodes
.iter()
.map(|node| node.id.as_str())
.filter(|id| !targets.contains(id))
.collect();
if roots.is_empty() {
return self
.nodes
.first()
.map(|node| vec![node.id.as_str()])
.unwrap_or_default();
}
roots
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct PipelineGraphMetadata {
#[serde(default)]
pub locked: bool,
#[serde(default)]
pub settings: PipelineGraphSettings,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct PipelineGraphSettings {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub invocation_retention: Option<PipelineInvocationRetention>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub trace_capture: Option<crate::pipeline::trace_capture::TraceCaptureSettings>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields)]
pub struct PipelineInvocationRetention {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_invocations: Option<u32>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub max_age_secs: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PipelineNode {
pub id: String,
pub kind: String,
#[serde(default)]
pub input_pins: Vec<String>,
#[serde(default)]
pub output_pins: Vec<String>,
#[serde(default)]
pub config: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(deny_unknown_fields)]
pub struct PipelineNote {
pub id: String,
#[serde(default)]
pub text: String,
#[serde(default)]
pub x: f64,
#[serde(default)]
pub y: f64,
#[serde(default)]
pub width: f64,
#[serde(default)]
pub height: f64,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub color: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PipelineEdge {
pub from_node: String,
pub from_pin: String,
pub to_node: String,
pub to_pin: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
#[serde(deny_unknown_fields)]
pub struct NodeScriptBridge {
pub name: String,
#[serde(default)]
pub enabled: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(deny_unknown_fields)]
pub struct NodeAiToolDefinition {
#[serde(default)]
pub registered: bool,
#[serde(default)]
pub tool_name: String,
#[serde(default)]
pub tool_description: String,
#[serde(default)]
pub tool_input_schema: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(deny_unknown_fields)]
pub struct NodeDefinition {
pub kind: String,
pub title: String,
pub description: String,
#[serde(default)]
pub config_schema: Value,
#[serde(default)]
pub input_schema: Value,
#[serde(default)]
pub output_schema: Value,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub examples: Vec<NodeExample>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub failure_semantics: Vec<NodeFailureSemantic>,
#[serde(default)]
pub input_pins: Vec<String>,
#[serde(default)]
pub output_pins: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub capabilities: Vec<NodeCapability>,
#[serde(default)]
pub script_available: bool,
#[serde(default)]
pub script_bridge: Option<NodeScriptBridge>,
#[serde(default)]
pub ai_tool: NodeAiToolDefinition,
#[serde(default)]
pub dsl_flags: Vec<DslFlag>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub fields: Vec<NodeFieldDef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub layout: Vec<LayoutItem>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub secret_paths: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub positional: Option<String>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub ui_category: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub ui_category_label: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(deny_unknown_fields)]
pub struct NodeExample {
#[serde(default, skip_serializing_if = "String::is_empty")]
pub title: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub dsl: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub description: String,
#[serde(default, skip_serializing_if = "Value::is_null")]
pub config: Value,
#[serde(default, skip_serializing_if = "Value::is_null")]
pub input: Value,
#[serde(default, skip_serializing_if = "Value::is_null")]
pub output: Value,
}
impl NodeExample {
pub fn dsl(title: &str, dsl: &str) -> Self {
Self { title: title.to_string(), dsl: dsl.to_string(), ..Default::default() }
}
pub fn output(mut self, output: Value) -> Self {
self.output = output;
self
}
pub fn input(mut self, input: Value) -> Self {
self.input = input;
self
}
pub fn note(mut self, description: &str) -> Self {
self.description = description.to_string();
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(deny_unknown_fields)]
pub struct NodeFailureSemantic {
pub code: String,
pub description: String,
#[serde(default)]
pub retryable: bool,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub retry_hint: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct NodeScriptUsageContract {
pub available: bool,
#[serde(default)]
pub bridge_name: String,
#[serde(default)]
pub enabled: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct NodeToolUsageContract {
pub registered: bool,
#[serde(default)]
pub tool_name: String,
#[serde(default)]
pub tool_description: String,
#[serde(default)]
pub tool_input_schema: Value,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct NodeUsageMatrix {
pub pipeline_node: bool,
pub script_usage: NodeScriptUsageContract,
pub tool_usage: NodeToolUsageContract,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct NodeContractItem {
pub kind: String,
pub title: String,
pub description: String,
#[serde(default)]
pub config_schema: Value,
#[serde(default)]
pub input_schema: Value,
#[serde(default)]
pub output_schema: Value,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub examples: Vec<NodeExample>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub failure_semantics: Vec<NodeFailureSemantic>,
#[serde(default)]
pub input_pins: Vec<String>,
#[serde(default)]
pub output_pins: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub capabilities: Vec<NodeCapability>,
pub usage_matrix: NodeUsageMatrix,
#[serde(default)]
pub dsl_flags: Vec<DslFlag>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub fields: Vec<NodeFieldDef>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub layout: Vec<LayoutItem>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub ui_category: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub ui_category_label: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub icon_url: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub icon_hash: String,
#[serde(default = "default_node_source")]
pub source: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub tier: String,
#[serde(default = "default_node_available")]
pub available: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub credential_requirements: Vec<NodeCredentialRequirement>,
}
fn default_node_available() -> bool {
true
}
fn default_node_source() -> String {
"native".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct NodeCredentialRequirement {
pub kind: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub title: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub description: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub config_key: String,
#[serde(default)]
pub required: bool,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub placeholder_names: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct NodeContractDocument {
pub ok: bool,
pub schema_version: &'static str,
pub source: &'static str,
#[serde(default)]
pub items: Vec<NodeContractItem>,
}
impl From<NodeDefinition> for NodeContractItem {
fn from(value: NodeDefinition) -> Self {
let (bridge_name, bridge_enabled) = value
.script_bridge
.as_ref()
.map(|bridge| (bridge.name.clone(), bridge.enabled))
.unwrap_or_else(|| (String::new(), false));
let source = default_node_source();
let ui_category = value.ui_category;
let ui_category_label = value.ui_category_label;
Self {
available: true,
kind: value.kind,
title: value.title,
description: value.description,
config_schema: value.config_schema,
input_schema: value.input_schema,
output_schema: value.output_schema,
examples: value.examples,
failure_semantics: value.failure_semantics,
input_pins: value.input_pins,
output_pins: value.output_pins,
capabilities: value.capabilities,
dsl_flags: value.dsl_flags,
fields: value.fields,
layout: value.layout,
usage_matrix: NodeUsageMatrix {
pipeline_node: true,
script_usage: NodeScriptUsageContract {
available: value.script_available,
bridge_name,
enabled: bridge_enabled,
},
tool_usage: NodeToolUsageContract {
registered: value.ai_tool.registered,
tool_name: value.ai_tool.tool_name,
tool_description: value.ai_tool.tool_description,
tool_input_schema: value.ai_tool.tool_input_schema,
},
},
ui_category,
ui_category_label,
icon_url: String::new(),
icon_hash: String::new(),
source,
tier: String::new(),
credential_requirements: Vec::new(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Signal {
pub kind: String,
pub message: String,
#[serde(default)]
pub node_id: String,
#[serde(default)]
pub node_kind: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub data: Option<serde_json::Value>,
#[serde(default)]
pub at: String,
}
pub type StepEvent = Signal;
#[derive(Debug, Clone)]
pub struct ExecutionBus {
tx: tokio::sync::broadcast::Sender<Signal>,
}
impl ExecutionBus {
pub fn new(capacity: usize) -> Self {
let (tx, _) = tokio::sync::broadcast::channel(capacity);
Self { tx }
}
pub fn emit(&self, signal: Signal) {
let _ = self.tx.send(signal);
}
pub fn subscribe(&self) -> tokio::sync::broadcast::Receiver<Signal> {
self.tx.subscribe()
}
}
#[derive(Debug, Clone, Default)]
pub struct ExecuteOptions {
pub bus: Option<std::sync::Arc<ExecutionBus>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipelineContext {
pub owner: String,
pub project: String,
pub pipeline: String,
pub request_id: String,
#[serde(default)]
pub route: String,
pub input: Value,
#[serde(default)]
pub trigger: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub placeholder: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NodeTraceEntry {
pub node_id: String,
pub node_kind: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub config: Option<Value>,
pub duration_ms: u64,
pub input: Value,
pub output: Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(default = "default_trace_status")]
pub status: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error_code: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub preview_snapshot: Option<Value>,
}
fn default_trace_status() -> String {
"ok".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipelineOutput {
pub value: Value,
pub trace: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub node_trace: Vec<NodeTraceEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PipelineError {
pub code: &'static str,
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub node_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub node_kind: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub node_trace: Vec<NodeTraceEntry>,
}
impl From<crate::zebfs::ZebFsError> for PipelineError {
fn from(err: crate::zebfs::ZebFsError) -> Self {
Self::new(err.code, err.message)
}
}
impl PipelineError {
pub fn new(code: &'static str, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
node_id: None,
node_kind: None,
node_trace: vec![],
}
}
pub fn node(node_id: &str, node_kind: &str, code: &'static str, msg: &str) -> Self {
Self {
code,
message: msg.to_string(),
node_id: Some(node_id.to_string()),
node_kind: Some(node_kind.to_string()),
node_trace: vec![],
}
}
}
impl Display for PipelineError {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.code, self.message)
}
}
impl std::error::Error for PipelineError {}