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