Skip to main content

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}