Skip to main content

simple_agents_workflow/yaml_runner/
contracts.rs

1use std::collections::HashMap;
2
3use async_trait::async_trait;
4use serde::{Deserialize, Serialize};
5use serde_json::Value;
6use simple_agent_type::message::Role;
7use simple_agent_type::tool::{ToolChoice, ToolType};
8use thiserror::Error;
9
10use super::{TraceContext, YamlToolTraceMode, YamlWorkflowTraceTenantContext};
11
12#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
13#[serde(rename_all = "snake_case")]
14pub enum YamlWorkflowTokenKind {
15    Output,
16    Thinking,
17}
18
19#[derive(Debug, Clone, PartialEq, Serialize)]
20pub struct YamlWorkflowEvent {
21    pub event_type: String,
22    pub node_id: Option<String>,
23    pub step_id: Option<String>,
24    pub node_kind: Option<String>,
25    pub streamable: Option<bool>,
26    pub message: Option<String>,
27    pub delta: Option<String>,
28    pub token_kind: Option<YamlWorkflowTokenKind>,
29    pub is_terminal_node_token: Option<bool>,
30    pub elapsed_ms: Option<u128>,
31    pub metadata: Option<Value>,
32}
33
34pub type WorkflowMessageRole = Role;
35
36#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
37pub struct WorkflowMessage {
38    pub role: WorkflowMessageRole,
39    pub content: String,
40    #[serde(default)]
41    pub name: Option<String>,
42    #[serde(default, alias = "toolCallId")]
43    pub tool_call_id: Option<String>,
44}
45
46#[derive(Debug, Clone, PartialEq, Serialize)]
47pub struct YamlTemplateBinding {
48    pub index: usize,
49    pub expression: String,
50    pub source_path: String,
51    pub resolved: Value,
52    pub resolved_type: String,
53    pub missing: bool,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
57pub enum YamlWorkflowDiagnosticSeverity {
58    Error,
59    Warning,
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
63pub struct YamlWorkflowDiagnostic {
64    pub node_id: Option<String>,
65    pub code: String,
66    pub severity: YamlWorkflowDiagnosticSeverity,
67    pub message: String,
68}
69
70#[derive(Debug, Error)]
71pub enum YamlWorkflowRunError {
72    #[error("failed to read workflow yaml '{path}': {source}")]
73    Read {
74        path: String,
75        source: std::io::Error,
76    },
77    #[error("failed to parse workflow yaml '{path}': {source}")]
78    Parse {
79        path: String,
80        source: serde_yaml::Error,
81    },
82    #[error("rejected workflow yaml '{path}': {reason}")]
83    FileRejected { path: String, reason: String },
84    #[error("workflow '{workflow_id}' has no nodes")]
85    EmptyNodes { workflow_id: String },
86    #[error("entry node '{entry_node}' does not exist")]
87    MissingEntry { entry_node: String },
88    #[error("unknown node id '{node_id}'")]
89    MissingNode { node_id: String },
90    #[error("unsupported node type in '{node_id}'")]
91    UnsupportedNodeType { node_id: String },
92    #[error("unsupported switch condition format: {condition}")]
93    UnsupportedCondition { condition: String },
94    #[error("switch node '{node_id}' has no valid next target")]
95    InvalidSwitchTarget { node_id: String },
96    #[error("llm returned non-object payload for node '{node_id}'")]
97    LlmPayloadNotObject { node_id: String },
98    #[error("custom worker handler '{handler}' is not supported")]
99    UnsupportedCustomHandler { handler: String },
100    #[error("llm execution failed for node '{node_id}': {message}")]
101    Llm { node_id: String, message: String },
102    #[error("custom worker execution failed for node '{node_id}': {message}")]
103    CustomWorker { node_id: String, message: String },
104    #[error("workflow validation failed with {diagnostics_count} error(s)")]
105    Validation {
106        diagnostics_count: usize,
107        diagnostics: Vec<YamlWorkflowDiagnostic>,
108    },
109    #[error("invalid workflow input: {message}")]
110    InvalidInput { message: String },
111    #[error("ir runtime execution failed: {message}")]
112    IrRuntime { message: String },
113    #[error("workflow event stream cancelled: {message}")]
114    EventSinkCancelled { message: String },
115}
116
117pub trait YamlWorkflowEventSink: Send + Sync {
118    fn emit(&self, event: &YamlWorkflowEvent);
119
120    fn is_cancelled(&self) -> bool {
121        false
122    }
123}
124
125pub struct NoopYamlWorkflowEventSink;
126
127impl YamlWorkflowEventSink for NoopYamlWorkflowEventSink {
128    fn emit(&self, _event: &YamlWorkflowEvent) {}
129}
130
131pub(super) fn workflow_event_sink_cancelled_message() -> &'static str {
132    "workflow event callback cancelled"
133}
134
135pub(super) fn event_sink_is_cancelled(event_sink: Option<&dyn YamlWorkflowEventSink>) -> bool {
136    event_sink.map(|sink| sink.is_cancelled()).unwrap_or(false)
137}
138
139#[derive(Debug, Clone, PartialEq, Eq, Error)]
140pub enum YamlToIrError {
141    #[error("entry node '{entry_node}' does not exist")]
142    MissingEntry { entry_node: String },
143    #[error("node '{node_id}' has multiple outgoing edges in YAML; IR llm/tool nodes require one")]
144    MultipleOutgoingEdge { node_id: String },
145    #[error("node '{node_id}' is unsupported for IR conversion: {reason}")]
146    UnsupportedNode { node_id: String, reason: String },
147}
148
149#[derive(Debug, Clone)]
150pub struct YamlLlmExecutionRequest {
151    pub node_id: String,
152    pub is_terminal_node: bool,
153    pub stream_json_as_text: bool,
154    pub model: String,
155    pub max_tokens: Option<u32>,
156    pub temperature: Option<f32>,
157    pub top_p: Option<f32>,
158    pub messages: Option<Vec<super::Message>>,
159    pub append_prompt_as_user: bool,
160    pub prompt: String,
161    pub prompt_template: String,
162    pub prompt_bindings: Vec<YamlTemplateBinding>,
163    pub schema: Value,
164    pub stream: bool,
165    pub heal: bool,
166    pub tools: Vec<YamlResolvedTool>,
167    pub tool_choice: Option<ToolChoice>,
168    pub max_tool_roundtrips: u8,
169    pub tool_calls_global_key: Option<String>,
170    pub tool_trace_mode: YamlToolTraceMode,
171    pub execution_context: Value,
172    pub email_text: String,
173    pub trace_id: Option<String>,
174    pub trace_context: Option<TraceContext>,
175    pub tenant_context: YamlWorkflowTraceTenantContext,
176    pub trace_sampled: bool,
177}
178
179#[derive(Debug, Clone)]
180pub struct YamlResolvedTool {
181    pub definition: super::ToolDefinition,
182    pub output_schema: Option<Value>,
183}
184
185#[async_trait]
186pub trait YamlWorkflowLlmExecutor: Send + Sync {
187    async fn complete_structured(
188        &self,
189        request: YamlLlmExecutionRequest,
190        event_sink: Option<&dyn YamlWorkflowEventSink>,
191    ) -> Result<super::YamlLlmExecutionResult, String>;
192}
193
194#[async_trait]
195pub trait YamlWorkflowCustomWorkerExecutor: Send + Sync {
196    async fn execute(
197        &self,
198        handler: &str,
199        handler_file: Option<&str>,
200        payload: &Value,
201        email_text: &str,
202        context: &Value,
203    ) -> Result<Value, String>;
204}
205
206#[derive(Debug, Clone, Deserialize)]
207pub struct YamlGlobalUpdate {
208    pub op: String,
209    pub from: Option<String>,
210    pub by: Option<f64>,
211}
212
213#[derive(Debug, Clone, Deserialize)]
214pub struct YamlNodeConfig {
215    pub prompt: Option<String>,
216    #[serde(default, alias = "schema")]
217    pub output_schema: Option<Value>,
218    pub payload: Option<Value>,
219    pub set_globals: Option<HashMap<String, String>>,
220    pub update_globals: Option<HashMap<String, YamlGlobalUpdate>>,
221}
222
223#[derive(Debug, Clone, Deserialize)]
224#[serde(deny_unknown_fields)]
225pub struct YamlCustomWorker {
226    pub handler: String,
227    pub handler_file: Option<String>,
228}
229
230#[derive(Debug, Clone, Deserialize)]
231pub struct YamlSwitchBranch {
232    pub condition: String,
233    pub target: String,
234}
235
236#[derive(Debug, Clone, Deserialize)]
237pub struct YamlSwitch {
238    #[serde(default)]
239    pub branches: Vec<YamlSwitchBranch>,
240    pub default: String,
241}
242
243#[derive(Debug, Clone, Deserialize)]
244#[serde(untagged)]
245pub enum YamlToolChoiceConfig {
246    Mode(super::ToolChoiceMode),
247    Function { function: String },
248    OpenAi(super::ToolChoiceTool),
249}
250
251#[derive(Debug, Clone, Deserialize)]
252pub struct YamlSimplifiedToolDeclaration {
253    pub name: String,
254    pub description: Option<String>,
255    pub input_schema: Value,
256    pub output_schema: Option<Value>,
257}
258
259#[derive(Debug, Clone, Deserialize)]
260pub struct YamlOpenAiToolFunction {
261    pub name: String,
262    pub description: Option<String>,
263    pub parameters: Option<Value>,
264    pub output_schema: Option<Value>,
265}
266
267#[derive(Debug, Clone, Deserialize)]
268pub struct YamlOpenAiToolDeclaration {
269    #[serde(rename = "type")]
270    pub tool_type: Option<ToolType>,
271    pub function: YamlOpenAiToolFunction,
272}
273
274#[derive(Debug, Clone, Deserialize)]
275#[serde(untagged)]
276pub enum YamlToolDeclaration {
277    OpenAi(YamlOpenAiToolDeclaration),
278    Simplified(YamlSimplifiedToolDeclaration),
279}
280
281#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, Default)]
282#[serde(rename_all = "snake_case")]
283pub enum YamlToolFormat {
284    #[default]
285    Openai,
286    Simplified,
287}
288
289#[derive(Debug, Clone, Deserialize)]
290#[serde(deny_unknown_fields)]
291pub struct YamlLlmCall {
292    pub model: String,
293    pub max_tokens: Option<u32>,
294    pub temperature: Option<f32>,
295    pub top_p: Option<f32>,
296    pub stream: Option<bool>,
297    pub stream_json_as_text: Option<bool>,
298    pub heal: Option<bool>,
299    pub messages_path: Option<String>,
300    pub append_prompt_as_user: Option<bool>,
301    #[serde(default)]
302    pub tools_format: YamlToolFormat,
303    #[serde(default)]
304    pub tools: Vec<YamlToolDeclaration>,
305    pub tool_choice: Option<YamlToolChoiceConfig>,
306    pub max_tool_roundtrips: Option<u8>,
307    pub tool_calls_global_key: Option<String>,
308}
309
310#[derive(Debug, Clone, Deserialize)]
311#[serde(deny_unknown_fields)]
312pub struct YamlNodeType {
313    pub llm_call: Option<YamlLlmCall>,
314    pub switch: Option<YamlSwitch>,
315    pub custom_worker: Option<YamlCustomWorker>,
316    pub end: Option<Value>,
317}
318
319#[derive(Debug, Clone, Deserialize)]
320pub struct YamlNode {
321    pub id: String,
322    pub node_type: YamlNodeType,
323    pub config: Option<YamlNodeConfig>,
324}
325
326impl YamlNode {
327    pub(super) fn kind_name(&self) -> &'static str {
328        if self.node_type.llm_call.is_some() {
329            "llm_call"
330        } else if self.node_type.switch.is_some() {
331            "switch"
332        } else if self.node_type.custom_worker.is_some() {
333            "custom_worker"
334        } else if self.node_type.end.is_some() {
335            "end"
336        } else {
337            "unknown"
338        }
339    }
340}
341
342#[derive(Debug, Clone, Deserialize)]
343pub struct YamlEdge {
344    pub from: String,
345    pub to: String,
346}
347
348#[derive(Debug, Clone, Deserialize)]
349pub struct YamlWorkflow {
350    pub id: String,
351    pub entry_node: String,
352    #[serde(default)]
353    pub nodes: Vec<YamlNode>,
354    #[serde(default)]
355    pub edges: Vec<YamlEdge>,
356}