metalcraft_flows/nodes.rs
1//! Typed views over each core node type's `data` payload.
2//!
3//! On the wire a [`crate::FlowNode`] carries `data` as a free-form
4//! `serde_json::Value` (§3 of the spec). These structs are *parse-on-demand*
5//! views: deserialize a node's `data` into the struct matching its
6//! [`crate::CoreNodeType`] to read it ergonomically and to validate its shape.
7//! They are never stored — the wire format remains the raw `Value`.
8
9use serde::{Deserialize, Serialize};
10use std::collections::BTreeMap;
11
12/// One declared invocation parameter on an [`entry`](crate::CoreNodeType::Entry)
13/// node. Seeds a flow variable at run start.
14#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
15pub struct InputSpec {
16 /// JSON type name (`"string"`, `"integer"`, `"boolean"`, …). Advisory.
17 #[serde(rename = "type")]
18 pub type_name: String,
19 /// Whether the caller must supply this input.
20 #[serde(default)]
21 pub required: bool,
22 /// Default used when the input is absent (and not `required`).
23 #[serde(default, skip_serializing_if = "Option::is_none")]
24 pub default: Option<serde_json::Value>,
25}
26
27/// `data` for an [`entry`](crate::CoreNodeType::Entry) node.
28#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
29pub struct EntryData {
30 /// `"manual" | "minutes" | "hours" | "cron"`.
31 #[serde(default = "default_schedule_type")]
32 pub schedule_type: String,
33 /// Interval for `"minutes"` / `"hours"` schedules.
34 #[serde(default, skip_serializing_if = "Option::is_none")]
35 pub interval: Option<u64>,
36 /// Cron expression for the `"cron"` schedule.
37 #[serde(default, skip_serializing_if = "Option::is_none")]
38 pub cron: Option<String>,
39 /// Optional typed invocation parameters, seeded into flow state at run start.
40 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub inputs: Option<BTreeMap<String, InputSpec>>,
42}
43
44fn default_schedule_type() -> String {
45 "manual".to_string()
46}
47
48/// `data` for a [`prompt`](crate::CoreNodeType::Prompt) node.
49#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
50pub struct PromptData {
51 /// The instruction (supports `{{…}}` interpolation).
52 pub prompt: String,
53 /// Persona to run as; falls back to the flow/runtime default.
54 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub persona: Option<String>,
56 /// Model override.
57 #[serde(default, skip_serializing_if = "Option::is_none")]
58 pub model: Option<String>,
59 /// Variable to store the final answer in.
60 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub output_var: Option<String>,
62 /// Optional JSON Schema; when set, the answer is parsed as structured output.
63 #[serde(default, skip_serializing_if = "Option::is_none")]
64 pub output_schema: Option<serde_json::Value>,
65}
66
67/// One predicate on a [`conditional`](crate::CoreNodeType::Conditional) node.
68#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
69pub struct Condition {
70 /// Output handle to take when this predicate matches.
71 pub handle: String,
72 /// State variable to read (dotted path, e.g. `_last`, `triage.severity`).
73 pub variable: String,
74 /// Operator wire name; see [`crate::eval::Operator`].
75 pub operator: String,
76 /// Right-hand comparison value (typed JSON; may be absent for `exists`).
77 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub value: Option<serde_json::Value>,
79}
80
81/// `data` for a [`conditional`](crate::CoreNodeType::Conditional) node.
82#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
83pub struct ConditionalData {
84 /// Ordered predicates; the first match wins.
85 pub conditions: Vec<Condition>,
86 /// Handle used when no predicate matches.
87 #[serde(default, skip_serializing_if = "Option::is_none")]
88 pub default_handle: Option<String>,
89}
90
91/// One typed output handle on a [`branch`](crate::CoreNodeType::Branch) node —
92/// a tool definition the classifier may select.
93#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
94pub struct BranchOutput {
95 /// Handle name (doubles as the tool name offered to the model).
96 pub handle: String,
97 /// Human/LLM-facing description of when to pick this handle.
98 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub description: Option<String>,
100 /// JSON Schema for this handle's payload (scalar or object).
101 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub schema: Option<serde_json::Value>,
103 /// Optional variable to also persist the payload into.
104 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub var: Option<String>,
106}
107
108/// The reserved output handle a [`branch`](crate::CoreNodeType::Branch) node
109/// takes on a **protocol failure** — an LLM/API error, a timeout, the agent
110/// step budget being exhausted with no selection, or a chosen handle whose
111/// payload does not satisfy its declared schema.
112///
113/// This mirrors the `error` handle emitted by `prompt`/`tool`/`http` nodes, so
114/// every executable node shares one failure convention. The rail is always
115/// available and **optional to wire**: a runtime routes to it on failure, and if
116/// nothing is wired to it (and no `default_handle` is set) the run fails loudly
117/// rather than reporting a false success. Because the rail carries a string
118/// reason, a `branch` output that explicitly declares this handle must type its
119/// `schema` as a string (or omit it) — see [`crate::validate`].
120pub const BRANCH_ERROR_HANDLE: &str = "error";
121
122/// `data` for a [`branch`](crate::CoreNodeType::Branch) node (the LLM classifier).
123#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
124pub struct BranchData {
125 /// The question/task the model answers by choosing an output.
126 pub query: String,
127 /// Typed output handles; the model must pick exactly one.
128 pub outputs: Vec<BranchOutput>,
129 /// Persona to run as (grants tools so the model can gather info first).
130 #[serde(default, skip_serializing_if = "Option::is_none")]
131 pub persona: Option<String>,
132 /// Model override.
133 #[serde(default, skip_serializing_if = "Option::is_none")]
134 pub model: Option<String>,
135 /// Legacy fallback handle for a protocol failure. When unset, the runtime
136 /// routes the reserved [`BRANCH_ERROR_HANDLE`] instead.
137 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub default_handle: Option<String>,
139 /// Seconds before treating the classification as failed.
140 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub timeout: Option<u64>,
142}
143
144/// `data` for a [`set_variable`](crate::CoreNodeType::SetVariable) node.
145#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
146pub struct SetVariableData {
147 /// Destination variable name.
148 pub variable: String,
149 /// Literal or `{{…}}`-templated value.
150 #[serde(default, skip_serializing_if = "Option::is_none")]
151 pub value: Option<serde_json::Value>,
152 /// Dotted path into `_last` to copy from (alternative to `value`).
153 #[serde(default, skip_serializing_if = "Option::is_none")]
154 pub from: Option<String>,
155}
156
157/// `data` for a [`tool`](crate::CoreNodeType::Tool) node.
158#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
159pub struct ToolData {
160 /// Registered tool to invoke directly.
161 pub tool_name: String,
162 /// Arguments object (values support `{{…}}` interpolation).
163 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub args: Option<serde_json::Value>,
165 /// Variable to store the tool result in.
166 #[serde(default, skip_serializing_if = "Option::is_none")]
167 pub output_var: Option<String>,
168}
169
170/// `data` for an [`http`](crate::CoreNodeType::Http) node.
171#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
172pub struct HttpData {
173 /// HTTP method (`GET`, `POST`, …).
174 pub method: String,
175 /// Target URL (supports `{{…}}` interpolation).
176 pub url: String,
177 /// Optional request headers.
178 #[serde(default, skip_serializing_if = "Option::is_none")]
179 pub headers: Option<serde_json::Value>,
180 /// Optional request body.
181 #[serde(default, skip_serializing_if = "Option::is_none")]
182 pub body: Option<serde_json::Value>,
183 /// Variable to store the response in.
184 #[serde(default, skip_serializing_if = "Option::is_none")]
185 pub output_var: Option<String>,
186}
187
188/// `data` for a [`sub_agent`](crate::CoreNodeType::SubAgent) node.
189#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
190pub struct SubAgentData {
191 /// Task for the sub-agent.
192 pub task: String,
193 /// Run as a named persona (preferred).
194 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub persona: Option<String>,
196 /// Otherwise a tool-set preset (`read_only` | `full` | `all`).
197 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub tool_set: Option<String>,
199 /// Scope integration tools to a single pack (with `tool_set = "all"`).
200 #[serde(default, skip_serializing_if = "Option::is_none")]
201 pub pack: Option<String>,
202 /// Variable to store the sub-agent's result in.
203 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub output_var: Option<String>,
205}
206
207/// `data` for an [`approval`](crate::CoreNodeType::Approval) node.
208#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
209pub struct ApprovalData {
210 /// Prompt shown to the human (supports `{{…}}` interpolation).
211 pub message: String,
212 /// Decision handles the human may choose; defaults to `["approve","reject"]`.
213 #[serde(default, skip_serializing_if = "Option::is_none")]
214 pub choices: Option<Vec<String>>,
215 /// Seconds before the approval times out.
216 #[serde(default, skip_serializing_if = "Option::is_none")]
217 pub timeout: Option<u64>,
218}
219
220/// `data` for a [`wait`](crate::CoreNodeType::Wait) node. Exactly one of
221/// `duration` / `until` should be set.
222#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
223pub struct WaitData {
224 /// Relative delay, e.g. `"2h"`, `"30m"`.
225 #[serde(default, skip_serializing_if = "Option::is_none")]
226 pub duration: Option<String>,
227 /// Absolute RFC-3339 timestamp to resume at.
228 #[serde(default, skip_serializing_if = "Option::is_none")]
229 pub until: Option<String>,
230}
231
232/// `data` for an [`end`](crate::CoreNodeType::End) node.
233#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
234pub struct EndData {
235 /// Terminal status label (defaults to `"completed"`).
236 #[serde(default, skip_serializing_if = "Option::is_none")]
237 pub status: Option<String>,
238 /// Values to publish as the flow's outputs.
239 #[serde(default, skip_serializing_if = "Option::is_none")]
240 pub outputs: Option<serde_json::Value>,
241}