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}