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}