@foresthubai/workflow-core 0.4.3 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (298) hide show
  1. package/LICENSE +202 -202
  2. package/NOTICE +14 -14
  3. package/README.md +63 -63
  4. package/dist/api/deployment.d.ts +74 -0
  5. package/dist/api/deployment.d.ts.map +1 -0
  6. package/dist/api/deployment.js +6 -0
  7. package/dist/api/deployment.js.map +1 -0
  8. package/dist/api/engine.d.ts +744 -0
  9. package/dist/api/engine.d.ts.map +1 -0
  10. package/dist/api/engine.js +6 -0
  11. package/dist/api/engine.js.map +1 -0
  12. package/dist/api/index.d.ts +4 -0
  13. package/dist/api/index.d.ts.map +1 -1
  14. package/dist/api/index.js +2 -0
  15. package/dist/api/index.js.map +1 -1
  16. package/dist/api/workflow.d.ts +25 -9
  17. package/dist/api/workflow.d.ts.map +1 -1
  18. package/dist/channel/Channel.d.ts +1 -1
  19. package/dist/channel/Channel.d.ts.map +1 -1
  20. package/dist/channel/Channel.js +3 -1
  21. package/dist/channel/Channel.js.map +1 -1
  22. package/dist/channel/ChannelDefinition.d.ts.map +1 -1
  23. package/dist/channel/ChannelDefinition.js +24 -0
  24. package/dist/channel/ChannelDefinition.js.map +1 -1
  25. package/dist/channel/index.d.ts.map +1 -1
  26. package/dist/channel/index.js +2 -0
  27. package/dist/channel/index.js.map +1 -1
  28. package/dist/channel/serialization.d.ts.map +1 -1
  29. package/dist/channel/serialization.js +8 -0
  30. package/dist/channel/serialization.js.map +1 -1
  31. package/dist/deploy/index.d.ts +5 -1
  32. package/dist/deploy/index.d.ts.map +1 -1
  33. package/dist/deploy/index.js +4 -1
  34. package/dist/deploy/index.js.map +1 -1
  35. package/dist/deploy/inputs.d.ts +26 -0
  36. package/dist/deploy/inputs.d.ts.map +1 -0
  37. package/dist/deploy/inputs.js +4 -0
  38. package/dist/deploy/inputs.js.map +1 -0
  39. package/dist/deploy/requirements.d.ts +24 -0
  40. package/dist/deploy/requirements.d.ts.map +1 -1
  41. package/dist/deploy/requirements.js +63 -0
  42. package/dist/deploy/requirements.js.map +1 -1
  43. package/dist/deploy/spec.d.ts +27 -0
  44. package/dist/deploy/spec.d.ts.map +1 -0
  45. package/dist/deploy/spec.js +352 -0
  46. package/dist/deploy/spec.js.map +1 -0
  47. package/dist/diagnostics/__fixtures__/diagnosticFixtures.d.ts.map +1 -1
  48. package/dist/diagnostics/__fixtures__/diagnosticFixtures.js +2 -0
  49. package/dist/diagnostics/__fixtures__/diagnosticFixtures.js.map +1 -1
  50. package/dist/diagnostics/diagnostics.d.ts.map +1 -1
  51. package/dist/diagnostics/diagnostics.js +2 -0
  52. package/dist/diagnostics/diagnostics.js.map +1 -1
  53. package/dist/diagnostics/index.d.ts.map +1 -1
  54. package/dist/diagnostics/index.js +2 -0
  55. package/dist/diagnostics/index.js.map +1 -1
  56. package/dist/edge/Edge.d.ts.map +1 -1
  57. package/dist/edge/Edge.js +2 -0
  58. package/dist/edge/Edge.js.map +1 -1
  59. package/dist/edge/EdgeDefinition.d.ts.map +1 -1
  60. package/dist/edge/EdgeDefinition.js +2 -0
  61. package/dist/edge/EdgeDefinition.js.map +1 -1
  62. package/dist/edge/EdgeType.d.ts.map +1 -1
  63. package/dist/edge/EdgeType.js +2 -0
  64. package/dist/edge/EdgeType.js.map +1 -1
  65. package/dist/edge/index.d.ts.map +1 -1
  66. package/dist/edge/index.js +2 -2
  67. package/dist/edge/index.js.map +1 -1
  68. package/dist/edge/serialization.d.ts.map +1 -1
  69. package/dist/edge/serialization.js +2 -0
  70. package/dist/edge/serialization.js.map +1 -1
  71. package/dist/expression/index.d.ts.map +1 -1
  72. package/dist/expression/index.js +2 -0
  73. package/dist/expression/index.js.map +1 -1
  74. package/dist/expression/parser.d.ts.map +1 -1
  75. package/dist/expression/parser.js +2 -0
  76. package/dist/expression/parser.js.map +1 -1
  77. package/dist/expression/types.d.ts.map +1 -1
  78. package/dist/expression/types.js +2 -0
  79. package/dist/expression/types.js.map +1 -1
  80. package/dist/function/FunctionDeclaration.d.ts.map +1 -1
  81. package/dist/function/FunctionDeclaration.js +2 -0
  82. package/dist/function/FunctionDeclaration.js.map +1 -1
  83. package/dist/function/index.d.ts.map +1 -1
  84. package/dist/function/index.js +2 -0
  85. package/dist/function/index.js.map +1 -1
  86. package/dist/function/serialization.d.ts.map +1 -1
  87. package/dist/function/serialization.js +2 -0
  88. package/dist/function/serialization.js.map +1 -1
  89. package/dist/id/index.d.ts.map +1 -1
  90. package/dist/id/index.js +2 -0
  91. package/dist/id/index.js.map +1 -1
  92. package/dist/index.d.ts.map +1 -1
  93. package/dist/index.js +2 -0
  94. package/dist/index.js.map +1 -1
  95. package/dist/memory/Memory.d.ts.map +1 -1
  96. package/dist/memory/Memory.js +2 -0
  97. package/dist/memory/Memory.js.map +1 -1
  98. package/dist/memory/MemoryDefinition.d.ts.map +1 -1
  99. package/dist/memory/MemoryDefinition.js +2 -0
  100. package/dist/memory/MemoryDefinition.js.map +1 -1
  101. package/dist/memory/MemoryFileDefinition.d.ts.map +1 -1
  102. package/dist/memory/MemoryFileDefinition.js +2 -0
  103. package/dist/memory/MemoryFileDefinition.js.map +1 -1
  104. package/dist/memory/MemoryRegistry.d.ts.map +1 -1
  105. package/dist/memory/MemoryRegistry.js +2 -0
  106. package/dist/memory/MemoryRegistry.js.map +1 -1
  107. package/dist/memory/VectorDatabaseDefinition.d.ts.map +1 -1
  108. package/dist/memory/VectorDatabaseDefinition.js +2 -0
  109. package/dist/memory/VectorDatabaseDefinition.js.map +1 -1
  110. package/dist/memory/index.d.ts.map +1 -1
  111. package/dist/memory/index.js +2 -0
  112. package/dist/memory/index.js.map +1 -1
  113. package/dist/memory/serialization.d.ts.map +1 -1
  114. package/dist/memory/serialization.js +2 -0
  115. package/dist/memory/serialization.js.map +1 -1
  116. package/dist/migration/index.d.ts.map +1 -1
  117. package/dist/migration/index.js +2 -0
  118. package/dist/migration/index.js.map +1 -1
  119. package/dist/migration/migrate.d.ts.map +1 -1
  120. package/dist/migration/migrate.js +2 -0
  121. package/dist/migration/migrate.js.map +1 -1
  122. package/dist/migration/migrations.d.ts.map +1 -1
  123. package/dist/migration/migrations.js +2 -0
  124. package/dist/migration/migrations.js.map +1 -1
  125. package/dist/migration/version.d.ts.map +1 -1
  126. package/dist/migration/version.js +2 -0
  127. package/dist/migration/version.js.map +1 -1
  128. package/dist/model/LLMModelDefinition.d.ts.map +1 -1
  129. package/dist/model/LLMModelDefinition.js +2 -0
  130. package/dist/model/LLMModelDefinition.js.map +1 -1
  131. package/dist/model/Model.d.ts.map +1 -1
  132. package/dist/model/Model.js +2 -13
  133. package/dist/model/Model.js.map +1 -1
  134. package/dist/model/ModelDefinition.d.ts.map +1 -1
  135. package/dist/model/ModelDefinition.js +2 -0
  136. package/dist/model/ModelDefinition.js.map +1 -1
  137. package/dist/model/ModelRegistry.d.ts.map +1 -1
  138. package/dist/model/ModelRegistry.js +2 -0
  139. package/dist/model/ModelRegistry.js.map +1 -1
  140. package/dist/model/index.d.ts.map +1 -1
  141. package/dist/model/index.js +2 -0
  142. package/dist/model/index.js.map +1 -1
  143. package/dist/model/serialization.d.ts.map +1 -1
  144. package/dist/model/serialization.js +2 -0
  145. package/dist/model/serialization.js.map +1 -1
  146. package/dist/node/AgentNode.d.ts.map +1 -1
  147. package/dist/node/AgentNode.js +2 -0
  148. package/dist/node/AgentNode.js.map +1 -1
  149. package/dist/node/DataNode.d.ts.map +1 -1
  150. package/dist/node/DataNode.js +2 -0
  151. package/dist/node/DataNode.js.map +1 -1
  152. package/dist/node/FunctionNode.d.ts.map +1 -1
  153. package/dist/node/FunctionNode.js +2 -0
  154. package/dist/node/FunctionNode.js.map +1 -1
  155. package/dist/node/InputNode.d.ts.map +1 -1
  156. package/dist/node/InputNode.js +2 -0
  157. package/dist/node/InputNode.js.map +1 -1
  158. package/dist/node/LogicNode.d.ts.map +1 -1
  159. package/dist/node/LogicNode.js +2 -0
  160. package/dist/node/LogicNode.js.map +1 -1
  161. package/dist/node/MqttNode.d.ts.map +1 -1
  162. package/dist/node/MqttNode.js +2 -0
  163. package/dist/node/MqttNode.js.map +1 -1
  164. package/dist/node/Node.d.ts.map +1 -1
  165. package/dist/node/Node.js +2 -7
  166. package/dist/node/Node.js.map +1 -1
  167. package/dist/node/NodeDefinition.d.ts.map +1 -1
  168. package/dist/node/NodeDefinition.js +2 -0
  169. package/dist/node/NodeDefinition.js.map +1 -1
  170. package/dist/node/NodeRegistry.d.ts.map +1 -1
  171. package/dist/node/NodeRegistry.js +2 -0
  172. package/dist/node/NodeRegistry.js.map +1 -1
  173. package/dist/node/OutputNode.d.ts.map +1 -1
  174. package/dist/node/OutputNode.js +4 -2
  175. package/dist/node/OutputNode.js.map +1 -1
  176. package/dist/node/ToolNode.d.ts.map +1 -1
  177. package/dist/node/ToolNode.js +2 -0
  178. package/dist/node/ToolNode.js.map +1 -1
  179. package/dist/node/TriggerNode.d.ts.map +1 -1
  180. package/dist/node/TriggerNode.js +2 -0
  181. package/dist/node/TriggerNode.js.map +1 -1
  182. package/dist/node/constants.d.ts.map +1 -1
  183. package/dist/node/constants.js +2 -0
  184. package/dist/node/constants.js.map +1 -1
  185. package/dist/node/index.d.ts.map +1 -1
  186. package/dist/node/index.js +2 -0
  187. package/dist/node/index.js.map +1 -1
  188. package/dist/node/methods.d.ts.map +1 -1
  189. package/dist/node/methods.js +2 -0
  190. package/dist/node/methods.js.map +1 -1
  191. package/dist/node/serialization.d.ts.map +1 -1
  192. package/dist/node/serialization.js +33 -13
  193. package/dist/node/serialization.js.map +1 -1
  194. package/dist/parameter/OutputParameter.d.ts.map +1 -1
  195. package/dist/parameter/OutputParameter.js +2 -0
  196. package/dist/parameter/OutputParameter.js.map +1 -1
  197. package/dist/parameter/Parameter.d.ts.map +1 -1
  198. package/dist/parameter/Parameter.js +2 -0
  199. package/dist/parameter/Parameter.js.map +1 -1
  200. package/dist/parameter/index.d.ts.map +1 -1
  201. package/dist/parameter/index.js +2 -0
  202. package/dist/parameter/index.js.map +1 -1
  203. package/dist/variable/Variable.d.ts.map +1 -1
  204. package/dist/variable/Variable.js +2 -0
  205. package/dist/variable/Variable.js.map +1 -1
  206. package/dist/variable/index.d.ts.map +1 -1
  207. package/dist/variable/index.js +2 -3
  208. package/dist/variable/index.js.map +1 -1
  209. package/dist/variable/operations.d.ts.map +1 -1
  210. package/dist/variable/operations.js +2 -0
  211. package/dist/variable/operations.js.map +1 -1
  212. package/dist/workflow/Workflow.d.ts.map +1 -1
  213. package/dist/workflow/Workflow.js +2 -0
  214. package/dist/workflow/Workflow.js.map +1 -1
  215. package/dist/workflow/index.d.ts.map +1 -1
  216. package/dist/workflow/index.js +2 -0
  217. package/dist/workflow/index.js.map +1 -1
  218. package/dist/workflow/serialization.d.ts.map +1 -1
  219. package/dist/workflow/serialization.js +2 -4
  220. package/dist/workflow/serialization.js.map +1 -1
  221. package/package.json +2 -2
  222. package/src/api/deployment.ts +74 -0
  223. package/src/api/engine.ts +744 -0
  224. package/src/api/index.ts +29 -11
  225. package/src/api/workflow.ts +25 -9
  226. package/src/channel/Channel.ts +14 -11
  227. package/src/channel/ChannelDefinition.ts +101 -76
  228. package/src/channel/index.ts +9 -6
  229. package/src/channel/serialization.ts +77 -68
  230. package/src/deploy/index.ts +17 -1
  231. package/src/deploy/inputs.ts +44 -0
  232. package/src/deploy/requirements.test.ts +64 -61
  233. package/src/deploy/requirements.ts +159 -41
  234. package/src/deploy/spec.test.ts +227 -0
  235. package/src/deploy/spec.ts +397 -0
  236. package/src/diagnostics/__fixtures__/diagnosticFixtures.ts +161 -158
  237. package/src/diagnostics/diagnostics.test.ts +881 -878
  238. package/src/diagnostics/diagnostics.ts +939 -936
  239. package/src/diagnostics/index.ts +14 -11
  240. package/src/edge/Edge.ts +26 -23
  241. package/src/edge/EdgeDefinition.ts +48 -45
  242. package/src/edge/EdgeType.ts +22 -19
  243. package/src/edge/index.ts +11 -8
  244. package/src/edge/serialization.ts +86 -83
  245. package/src/expression/index.ts +7 -4
  246. package/src/expression/parser.ts +365 -362
  247. package/src/expression/types.ts +33 -30
  248. package/src/function/FunctionDeclaration.ts +57 -54
  249. package/src/function/index.ts +6 -3
  250. package/src/function/serialization.ts +43 -40
  251. package/src/globals.d.ts +12 -9
  252. package/src/id/index.ts +11 -8
  253. package/src/index.ts +25 -22
  254. package/src/memory/Memory.ts +18 -15
  255. package/src/memory/MemoryDefinition.ts +19 -16
  256. package/src/memory/MemoryFileDefinition.ts +40 -37
  257. package/src/memory/MemoryRegistry.ts +38 -35
  258. package/src/memory/VectorDatabaseDefinition.ts +24 -21
  259. package/src/memory/index.ts +11 -8
  260. package/src/memory/serialization.ts +50 -47
  261. package/src/migration/index.ts +7 -4
  262. package/src/migration/migrate.test.ts +47 -44
  263. package/src/migration/migrate.ts +61 -58
  264. package/src/migration/migrations.ts +27 -24
  265. package/src/migration/version.ts +12 -9
  266. package/src/model/LLMModelDefinition.ts +15 -12
  267. package/src/model/Model.ts +42 -39
  268. package/src/model/ModelDefinition.ts +18 -15
  269. package/src/model/ModelRegistry.ts +36 -33
  270. package/src/model/index.ts +10 -7
  271. package/src/model/serialization.ts +33 -30
  272. package/src/node/AgentNode.ts +85 -82
  273. package/src/node/DataNode.ts +44 -41
  274. package/src/node/FunctionNode.ts +79 -76
  275. package/src/node/InputNode.ts +188 -185
  276. package/src/node/LogicNode.ts +36 -33
  277. package/src/node/MqttNode.ts +130 -127
  278. package/src/node/Node.ts +64 -61
  279. package/src/node/NodeDefinition.ts +40 -37
  280. package/src/node/NodeRegistry.ts +88 -85
  281. package/src/node/OutputNode.ts +90 -87
  282. package/src/node/ToolNode.ts +35 -32
  283. package/src/node/TriggerNode.ts +275 -272
  284. package/src/node/constants.ts +19 -16
  285. package/src/node/index.ts +29 -26
  286. package/src/node/methods.ts +281 -278
  287. package/src/node/serialization.test.ts +51 -0
  288. package/src/node/serialization.ts +565 -544
  289. package/src/parameter/OutputParameter.ts +71 -68
  290. package/src/parameter/Parameter.ts +246 -243
  291. package/src/parameter/index.ts +36 -33
  292. package/src/variable/Variable.ts +13 -10
  293. package/src/variable/index.ts +19 -16
  294. package/src/variable/operations.ts +109 -106
  295. package/src/workflow/Workflow.ts +44 -41
  296. package/src/workflow/index.ts +6 -3
  297. package/src/workflow/serialization.test.ts +243 -240
  298. package/src/workflow/serialization.ts +245 -242
@@ -1,544 +1,565 @@
1
- import type { Schemas } from "../api";
2
- import type { NodeData, Node } from "./Node";
3
- import type { Expression } from "../api";
4
- import type { FunctionInfo } from "../function";
5
- import type { OutputBinding, OutputDeclaration } from "../parameter";
6
- import { pruneArguments } from "../parameter";
7
- import { NodeRegistry } from "./NodeRegistry";
8
-
9
- export type ApiNode = Schemas["Node"];
10
-
11
- /**
12
- * Resolve a function's signature snapshot by id. A `FunctionCall` on the wire stores
13
- * only `functionId`; deserialize rebuilds the in-memory `functionInfo` snapshot
14
- * (which core's variable helpers + staleness read) from the workflow's function
15
- * table via this. Unknown id (e.g. a call to a since-deleted function) → a minimal
16
- * stub so the node still round-trips and surfaces as deleted.
17
- */
18
- export type ResolveFunctionInfo = (functionId: string) => FunctionInfo | undefined;
19
-
20
- /**
21
- * Serialize a domain Node to the strict API format (Schemas["Node"]).
22
- * Strips hidden parameters (those whose activationRules are not met). The
23
- * `isToolInput` flag is threaded into activation evaluation so rules like
24
- * `isControlFlow` / `isToolInput` resolve correctly per-instance.
25
- */
26
- export function serialize(node: Node, isToolInput: boolean): ApiNode {
27
- const result = serializeNodeData(node, node.position, isToolInput);
28
- if (node.label) {
29
- result.label = node.label;
30
- }
31
-
32
- // serializeNode emits gated/optional params uniformly; this pass prunes the
33
- // ones that must not reach the api — inactive for this instance, or empty
34
- // optionals (so the consumer's presence check sees absent, not `""`/`null`).
35
- // FunctionCall gates its own params inline (its bindings have a different api
36
- // shape), so it's excluded here.
37
- if ("arguments" in result && result.arguments) {
38
- const def = node.type !== "FunctionCall" ? NodeRegistry.getByType(node.type) : undefined;
39
- if (def) {
40
- pruneArguments(result.arguments, def.parameters, isToolInput);
41
- }
42
- }
43
-
44
- return result;
45
- }
46
-
47
- function serializeNodeData(data: NodeData, position: { x: number; y: number }, isToolInput: boolean): Schemas["Node"] {
48
- switch (data.type) {
49
- case "ReadPin":
50
- return {
51
- id: data.id,
52
- type: data.type,
53
- position: position,
54
- arguments: {
55
- pinReference: data.arguments.pinReference!,
56
- signalType: data.arguments.signalType,
57
- output: data.arguments.output,
58
- toolDescription: data.arguments.toolDescription,
59
- },
60
- };
61
- case "SerialRead":
62
- return {
63
- id: data.id,
64
- type: data.type,
65
- position: position,
66
- arguments: {
67
- portReference: data.arguments.portReference!,
68
- ...(data.arguments.prompt !== undefined ? { prompt: data.arguments.prompt } : {}),
69
- output: data.arguments.output,
70
- },
71
- };
72
- case "WritePin":
73
- return {
74
- id: data.id,
75
- type: data.type,
76
- position: position,
77
- arguments: {
78
- pinReference: data.arguments.pinReference!,
79
- signalType: data.arguments.signalType,
80
- value: data.arguments.value,
81
- },
82
- };
83
- case "SerialWrite":
84
- return {
85
- id: data.id,
86
- type: data.type,
87
- position: position,
88
- arguments: {
89
- portReference: data.arguments.portReference!,
90
- value: data.arguments.value,
91
- },
92
- };
93
- case "Agent": {
94
- // outputDeclarations is a list both in domain and API. Each entry's `name`
95
- // is the JSON property the LLM is asked to produce; uniqueness is enforced
96
- // by diagnostics, not the schema. memoryRefs is also a 1:1 list — domain
97
- // and API share the same MemoryRef shape.
98
- return {
99
- id: data.id,
100
- type: data.type,
101
- position: position,
102
- arguments: {
103
- name: data.arguments.name,
104
- model: data.arguments.model,
105
- instructions: data.arguments.instructions,
106
- maxTurns: data.arguments.maxTurns,
107
- outputDeclarations: data.arguments.outputDeclarations,
108
- memoryRefs: data.arguments.memoryRefs ?? [],
109
- answer: data.arguments.answer,
110
- toolDescription: data.arguments.toolDescription,
111
- },
112
- };
113
- }
114
- case "If":
115
- return {
116
- id: data.id,
117
- type: data.type,
118
- position: position,
119
- arguments: {
120
- condition: data.arguments.condition,
121
- },
122
- };
123
- case "OnFunctionCall":
124
- return {
125
- id: data.id,
126
- type: data.type,
127
- position: position,
128
- };
129
- case "OnStartup":
130
- return {
131
- id: data.id,
132
- type: data.type,
133
- position: position,
134
- };
135
- case "OnPinEdge":
136
- return {
137
- id: data.id,
138
- type: data.type,
139
- position: position,
140
- arguments: {
141
- pinReference: data.arguments.pinReference!,
142
- edge: data.arguments.edge,
143
- },
144
- };
145
- case "OnSerialReceive":
146
- return {
147
- id: data.id,
148
- type: data.type,
149
- position: position,
150
- arguments: {
151
- portReference: data.arguments.portReference!,
152
- output: data.arguments.output,
153
- },
154
- };
155
- case "OnThreshold":
156
- return {
157
- id: data.id,
158
- type: data.type,
159
- position: position,
160
- arguments: {
161
- variable: data.arguments.variable!,
162
- threshold: data.arguments.threshold!,
163
- direction: data.arguments.direction,
164
- deadband: data.arguments.deadband,
165
- output: data.arguments.output,
166
- },
167
- };
168
- case "Delay":
169
- return {
170
- id: data.id,
171
- type: data.type,
172
- position: position,
173
- arguments: {
174
- delayMs: data.arguments.delayMs!,
175
- },
176
- };
177
- case "Ticker":
178
- return {
179
- id: data.id,
180
- type: data.type,
181
- position: position,
182
- arguments: {
183
- intervalValue: data.arguments.intervalValue!,
184
- intervalUnit: data.arguments.intervalUnit,
185
- },
186
- };
187
- case "Alarm":
188
- return {
189
- id: data.id,
190
- type: data.type,
191
- position: position,
192
- arguments: {
193
- time: data.arguments.time,
194
- days: data.arguments.days,
195
- },
196
- };
197
- case "WebSearchTool":
198
- return {
199
- id: data.id,
200
- type: data.type,
201
- position: position,
202
- arguments: {
203
- maxResults: data.arguments.maxResults,
204
- },
205
- };
206
- case "Retriever":
207
- return {
208
- id: data.id,
209
- type: data.type,
210
- position: position,
211
- arguments: {
212
- memoryReference: data.arguments.memoryReference,
213
- topK: data.arguments.topK!,
214
- query: data.arguments.query,
215
- output: data.arguments.output,
216
- toolDescription: data.arguments.toolDescription,
217
- },
218
- };
219
- case "WebFetch":
220
- return {
221
- id: data.id,
222
- type: data.type,
223
- position: position,
224
- arguments: {
225
- url: data.arguments.url,
226
- maxChars: data.arguments.maxChars,
227
- output: data.arguments.output,
228
- },
229
- };
230
- case "FunctionCall": {
231
- // Frontend stores FunctionCall args flat (unified with every other node), but
232
- // the API schema keeps the nested { inputBindings, outputBindings } shape.
233
- // Translate here so the api format stays stable. `toolDescription` sits
234
- // alongside the bindings at the api level and is only emitted when the
235
- // node is currently wired as a tool (exec-mode calls don't need it).
236
- const inputBindings: Record<string, Expression> = {};
237
- const outputBindings: Record<string, OutputBinding> = {};
238
- const args = data.arguments as Record<string, unknown>;
239
- for (const arg of data.functionInfo.arguments) {
240
- const key = arg.uid ?? arg.name;
241
- const v = args[key];
242
- if (v !== undefined) inputBindings[key] = v as Expression;
243
- }
244
- for (const ret of data.functionInfo.returns) {
245
- const key = ret.uid ?? ret.name;
246
- const v = args[key];
247
- if (v !== undefined) outputBindings[key] = v as OutputBinding;
248
- }
249
- const toolDescription = args.toolDescription as string | undefined;
250
- return {
251
- id: data.id,
252
- type: data.type,
253
- functionId: data.functionInfo.id,
254
- position: position,
255
- arguments: {
256
- inputBindings,
257
- outputBindings,
258
- ...(isToolInput && toolDescription !== undefined ? { toolDescription } : {}),
259
- },
260
- };
261
- }
262
- case "SetVariable":
263
- return {
264
- id: data.id,
265
- type: data.type,
266
- position: position,
267
- arguments: {
268
- variable: data.arguments.variable!,
269
- value: data.arguments.value,
270
- },
271
- };
272
- case "MqttPublish":
273
- return {
274
- id: data.id,
275
- type: data.type,
276
- position: position,
277
- arguments: {
278
- channelReference: data.arguments.channelReference ?? "",
279
- dataType: data.arguments.dataType,
280
- value: data.arguments.value,
281
- qos: Number(data.arguments.qos) as 0 | 1 | 2,
282
- retain: data.arguments.retain,
283
- },
284
- };
285
- case "OnMqttMessage":
286
- return {
287
- id: data.id,
288
- type: data.type,
289
- position: position,
290
- arguments: {
291
- channelReference: data.arguments.channelReference ?? "",
292
- dataType: data.arguments.dataType,
293
- output: data.arguments.output,
294
- },
295
- };
296
- }
297
- }
298
-
299
- /**
300
- * Convert a strict API Node to a domain Node (NodeData + position). `resolveFunctionInfo`
301
- * is required only for `FunctionCall` nodes see {@link ResolveFunctionInfo}.
302
- */
303
- export function deserialize(apiNode: ApiNode, resolveFunctionInfo?: ResolveFunctionInfo): Node {
304
- return { ...deserializeNodeData(apiNode, resolveFunctionInfo), position: apiNode.position };
305
- }
306
-
307
- /** Build the NodeData payload from an API Node (no position). */
308
- function deserializeNodeData(apiNode: Schemas["Node"], resolveFunctionInfo?: ResolveFunctionInfo): NodeData {
309
- switch (apiNode.type) {
310
- case "ReadPin":
311
- return {
312
- id: apiNode.id,
313
- type: apiNode.type,
314
- label: apiNode.label,
315
- arguments: {
316
- pinReference: apiNode.arguments.pinReference ?? "",
317
- signalType: apiNode.arguments.signalType,
318
- output: apiNode.arguments.output as OutputBinding,
319
- toolDescription: apiNode.arguments.toolDescription,
320
- },
321
- };
322
- case "SerialRead":
323
- return {
324
- id: apiNode.id,
325
- type: apiNode.type,
326
- label: apiNode.label,
327
- arguments: {
328
- portReference: apiNode.arguments.portReference ?? "",
329
- prompt: apiNode.arguments.prompt ?? "",
330
- output: apiNode.arguments.output as OutputBinding,
331
- },
332
- };
333
- case "Retriever":
334
- return {
335
- id: apiNode.id,
336
- type: apiNode.type,
337
- label: apiNode.label,
338
- arguments: {
339
- memoryReference: apiNode.arguments.memoryReference ?? "",
340
- topK: apiNode.arguments.topK ?? 0,
341
- query: apiNode.arguments.query,
342
- output: apiNode.arguments.output as OutputBinding,
343
- toolDescription: apiNode.arguments.toolDescription,
344
- },
345
- };
346
- case "WritePin":
347
- return {
348
- id: apiNode.id,
349
- type: apiNode.type,
350
- label: apiNode.label,
351
- arguments: {
352
- pinReference: apiNode.arguments.pinReference ?? "",
353
- signalType: apiNode.arguments.signalType,
354
- value: apiNode.arguments.value,
355
- },
356
- };
357
- case "SerialWrite":
358
- return {
359
- id: apiNode.id,
360
- type: apiNode.type,
361
- label: apiNode.label,
362
- arguments: {
363
- portReference: apiNode.arguments.portReference ?? "",
364
- value: apiNode.arguments.value,
365
- },
366
- };
367
- case "Agent":
368
- return {
369
- id: apiNode.id,
370
- type: apiNode.type,
371
- label: apiNode.label,
372
- arguments: {
373
- name: apiNode.arguments.name ?? "",
374
- model: apiNode.arguments.model ?? "",
375
- instructions: apiNode.arguments.instructions ?? "",
376
- maxTurns: apiNode.arguments.maxTurns,
377
- outputDeclarations: apiNode.arguments.outputDeclarations as OutputDeclaration[],
378
- memoryRefs: apiNode.arguments.memoryRefs ?? [],
379
- answer: apiNode.arguments.answer as OutputBinding,
380
- toolDescription: apiNode.arguments.toolDescription,
381
- },
382
- };
383
- case "If":
384
- return {
385
- id: apiNode.id,
386
- type: apiNode.type,
387
- label: apiNode.label,
388
- arguments: {
389
- condition: apiNode.arguments.condition,
390
- },
391
- };
392
- case "OnFunctionCall":
393
- return { id: apiNode.id, type: apiNode.type, label: apiNode.label, arguments: {} };
394
- case "OnStartup":
395
- return { id: apiNode.id, type: apiNode.type, label: apiNode.label, arguments: {} };
396
- case "OnPinEdge":
397
- return {
398
- id: apiNode.id,
399
- type: apiNode.type,
400
- label: apiNode.label,
401
- arguments: {
402
- pinReference: apiNode.arguments.pinReference ?? "",
403
- edge: apiNode.arguments.edge,
404
- },
405
- };
406
- case "OnSerialReceive":
407
- return {
408
- id: apiNode.id,
409
- type: apiNode.type,
410
- label: apiNode.label,
411
- arguments: {
412
- portReference: apiNode.arguments.portReference ?? "",
413
- output: apiNode.arguments.output as OutputBinding,
414
- },
415
- };
416
- case "OnThreshold":
417
- return {
418
- id: apiNode.id,
419
- type: apiNode.type,
420
- label: apiNode.label,
421
- arguments: {
422
- variable: apiNode.arguments.variable,
423
- threshold: apiNode.arguments.threshold,
424
- direction: apiNode.arguments.direction,
425
- deadband: apiNode.arguments.deadband,
426
- output: apiNode.arguments.output as OutputBinding,
427
- },
428
- };
429
- case "Delay":
430
- return {
431
- id: apiNode.id,
432
- type: apiNode.type,
433
- label: apiNode.label,
434
- arguments: {
435
- delayMs: apiNode.arguments.delayMs ?? 0,
436
- },
437
- };
438
- case "Ticker":
439
- return {
440
- id: apiNode.id,
441
- type: apiNode.type,
442
- label: apiNode.label,
443
- arguments: {
444
- intervalValue: apiNode.arguments.intervalValue ?? 0,
445
- intervalUnit: apiNode.arguments.intervalUnit,
446
- },
447
- };
448
- case "Alarm":
449
- return {
450
- id: apiNode.id,
451
- type: apiNode.type,
452
- label: apiNode.label,
453
- arguments: {
454
- time: apiNode.arguments.time ?? "",
455
- days: apiNode.arguments.days,
456
- },
457
- };
458
- case "WebSearchTool":
459
- return {
460
- id: apiNode.id,
461
- type: apiNode.type,
462
- label: apiNode.label,
463
- arguments: {
464
- maxResults: apiNode.arguments.maxResults,
465
- },
466
- };
467
- case "WebFetch":
468
- return {
469
- id: apiNode.id,
470
- type: apiNode.type,
471
- label: apiNode.label,
472
- arguments: {
473
- url: apiNode.arguments.url,
474
- maxChars: apiNode.arguments.maxChars,
475
- output: apiNode.arguments.output as OutputBinding,
476
- },
477
- };
478
- case "SetVariable":
479
- return {
480
- id: apiNode.id,
481
- type: apiNode.type,
482
- label: apiNode.label,
483
- arguments: {
484
- variable: apiNode.arguments.variable,
485
- value: apiNode.arguments.value,
486
- },
487
- };
488
- case "FunctionCall": {
489
- // Lift the api's nested { inputBindings, outputBindings } into the flat
490
- // domain arguments record. Uid collisions are impossible within a single
491
- // function (one namespace across args + returns). `toolDescription`
492
- // sits at the same level in the api and is folded into the flat bag
493
- // under the reserved `toolDescription` key.
494
- const flat: Record<string, Expression | OutputBinding | string> = {
495
- ...((apiNode.arguments.inputBindings ?? {}) as Record<string, Expression>),
496
- ...((apiNode.arguments.outputBindings ?? {}) as Record<string, OutputBinding>),
497
- };
498
- if (apiNode.arguments.toolDescription !== undefined) {
499
- flat.toolDescription = apiNode.arguments.toolDescription;
500
- }
501
- // The wire carries only `functionId`; rebuild the in-memory signature snapshot
502
- // from the workflow's function table. A missing function (deleted/hand-edited)
503
- // gets a minimal stub so the node still loads and surfaces as deleted.
504
- const functionInfo: FunctionInfo = resolveFunctionInfo?.(apiNode.functionId) ?? {
505
- id: apiNode.functionId,
506
- version: 0,
507
- name: "",
508
- arguments: [],
509
- returns: [],
510
- };
511
- return {
512
- id: apiNode.id,
513
- type: apiNode.type,
514
- label: apiNode.label,
515
- functionInfo,
516
- arguments: flat,
517
- };
518
- }
519
- case "MqttPublish":
520
- return {
521
- id: apiNode.id,
522
- type: apiNode.type,
523
- label: apiNode.label,
524
- arguments: {
525
- channelReference: apiNode.arguments.channelReference ?? "",
526
- dataType: apiNode.arguments.dataType,
527
- value: apiNode.arguments.value,
528
- qos: String(apiNode.arguments.qos) as "0" | "1" | "2",
529
- retain: apiNode.arguments.retain,
530
- },
531
- };
532
- case "OnMqttMessage":
533
- return {
534
- id: apiNode.id,
535
- type: apiNode.type,
536
- label: apiNode.label,
537
- arguments: {
538
- channelReference: apiNode.arguments.channelReference ?? "",
539
- dataType: apiNode.arguments.dataType,
540
- output: apiNode.arguments.output as OutputBinding,
541
- },
542
- };
543
- }
544
- }
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) 2026 ForestHub.
3
+
4
+ import type { Schemas } from "../api";
5
+ import type { NodeData, Node } from "./Node";
6
+ import type { Expression } from "../api";
7
+ import type { FunctionInfo } from "../function";
8
+ import type { OutputBinding, OutputDeclaration } from "../parameter";
9
+ import { pruneArguments } from "../parameter";
10
+ import { NodeRegistry } from "./NodeRegistry";
11
+
12
+ export type ApiNode = Schemas["Node"];
13
+
14
+ /**
15
+ * Resolve a function's signature snapshot by id. A `FunctionCall` on the wire stores
16
+ * only `functionId`; deserialize rebuilds the in-memory `functionInfo` snapshot
17
+ * (which core's variable helpers + staleness read) from the workflow's function
18
+ * table via this. Unknown id (e.g. a call to a since-deleted function) a minimal
19
+ * stub so the node still round-trips and surfaces as deleted.
20
+ */
21
+ export type ResolveFunctionInfo = (functionId: string) => FunctionInfo | undefined;
22
+
23
+ /**
24
+ * A required wire field the workflow may not have yet: drafts save on purpose,
25
+ * and deserialize mirrors this with `??` fallbacks. serialize() emits such a
26
+ * field as absent (see the undefined-prune there); validate / check-schema /
27
+ * deploy are the strictness gates. This is the one deliberate spot where the
28
+ * api's "required" is relaxed — use it instead of `!` assertions.
29
+ */
30
+ function draft<T>(value: T | undefined): T {
31
+ return value as T;
32
+ }
33
+
34
+ /**
35
+ * Serialize a domain Node to the strict API format (Schemas["Node"]).
36
+ * Strips hidden parameters (those whose activationRules are not met). The
37
+ * `isToolInput` flag is threaded into activation evaluation so rules like
38
+ * `isControlFlow` / `isToolInput` resolve correctly per-instance.
39
+ */
40
+ export function serialize(node: Node, isToolInput: boolean): ApiNode {
41
+ const result = serializeNodeData(node, node.position, isToolInput);
42
+ if (node.label) {
43
+ result.label = node.label;
44
+ }
45
+
46
+ // serializeNode emits gated/optional params uniformly; this pass prunes the
47
+ // ones that must not reach the api inactive for this instance, or empty
48
+ // optionals (so the consumer's presence check sees absent, not `""`/`null`).
49
+ // FunctionCall gates its own params inline (its bindings have a different api
50
+ // shape), so it's excluded here.
51
+ if ("arguments" in result && result.arguments) {
52
+ const def = node.type !== "FunctionCall" ? NodeRegistry.getByType(node.type) : undefined;
53
+ if (def) {
54
+ pruneArguments(result.arguments, def.parameters, isToolInput);
55
+ }
56
+ // Required-but-unset draft arguments (see draft()) are emitted as absent,
57
+ // so the in-memory node matches its JSON form.
58
+ const args = result.arguments as Record<string, unknown>;
59
+ for (const key of Object.keys(args)) {
60
+ if (args[key] === undefined) delete args[key];
61
+ }
62
+ }
63
+
64
+ return result;
65
+ }
66
+
67
+ function serializeNodeData(data: NodeData, position: { x: number; y: number }, isToolInput: boolean): Schemas["Node"] {
68
+ switch (data.type) {
69
+ case "ReadPin":
70
+ return {
71
+ id: data.id,
72
+ type: data.type,
73
+ position: position,
74
+ arguments: {
75
+ pinReference: draft(data.arguments.pinReference),
76
+ signalType: data.arguments.signalType,
77
+ output: data.arguments.output,
78
+ toolDescription: data.arguments.toolDescription,
79
+ },
80
+ };
81
+ case "SerialRead":
82
+ return {
83
+ id: data.id,
84
+ type: data.type,
85
+ position: position,
86
+ arguments: {
87
+ portReference: draft(data.arguments.portReference),
88
+ ...(data.arguments.prompt !== undefined ? { prompt: data.arguments.prompt } : {}),
89
+ output: data.arguments.output,
90
+ },
91
+ };
92
+ case "WritePin":
93
+ return {
94
+ id: data.id,
95
+ type: data.type,
96
+ position: position,
97
+ arguments: {
98
+ pinReference: draft(data.arguments.pinReference),
99
+ signalType: data.arguments.signalType,
100
+ value: data.arguments.value,
101
+ },
102
+ };
103
+ case "SerialWrite":
104
+ return {
105
+ id: data.id,
106
+ type: data.type,
107
+ position: position,
108
+ arguments: {
109
+ portReference: draft(data.arguments.portReference),
110
+ value: data.arguments.value,
111
+ },
112
+ };
113
+ case "Agent": {
114
+ // outputDeclarations is a list both in domain and API. Each entry's `name`
115
+ // is the JSON property the LLM is asked to produce; uniqueness is enforced
116
+ // by diagnostics, not the schema. memoryRefs is also a 1:1 list — domain
117
+ // and API share the same MemoryRef shape.
118
+ return {
119
+ id: data.id,
120
+ type: data.type,
121
+ position: position,
122
+ arguments: {
123
+ name: data.arguments.name,
124
+ model: data.arguments.model,
125
+ instructions: data.arguments.instructions,
126
+ maxTurns: data.arguments.maxTurns,
127
+ outputDeclarations: data.arguments.outputDeclarations,
128
+ memoryRefs: data.arguments.memoryRefs ?? [],
129
+ answer: data.arguments.answer,
130
+ toolDescription: data.arguments.toolDescription,
131
+ },
132
+ };
133
+ }
134
+ case "If":
135
+ return {
136
+ id: data.id,
137
+ type: data.type,
138
+ position: position,
139
+ arguments: {
140
+ condition: data.arguments.condition,
141
+ },
142
+ };
143
+ case "OnFunctionCall":
144
+ return {
145
+ id: data.id,
146
+ type: data.type,
147
+ position: position,
148
+ };
149
+ case "OnStartup":
150
+ return {
151
+ id: data.id,
152
+ type: data.type,
153
+ position: position,
154
+ };
155
+ case "OnPinEdge":
156
+ return {
157
+ id: data.id,
158
+ type: data.type,
159
+ position: position,
160
+ arguments: {
161
+ pinReference: draft(data.arguments.pinReference),
162
+ edge: data.arguments.edge,
163
+ },
164
+ };
165
+ case "OnSerialReceive":
166
+ return {
167
+ id: data.id,
168
+ type: data.type,
169
+ position: position,
170
+ arguments: {
171
+ portReference: draft(data.arguments.portReference),
172
+ output: data.arguments.output,
173
+ },
174
+ };
175
+ case "OnThreshold":
176
+ return {
177
+ id: data.id,
178
+ type: data.type,
179
+ position: position,
180
+ arguments: {
181
+ variable: draft(data.arguments.variable),
182
+ threshold: draft(data.arguments.threshold),
183
+ direction: data.arguments.direction,
184
+ deadband: data.arguments.deadband,
185
+ output: data.arguments.output,
186
+ },
187
+ };
188
+ case "Delay":
189
+ return {
190
+ id: data.id,
191
+ type: data.type,
192
+ position: position,
193
+ arguments: {
194
+ delayMs: draft(data.arguments.delayMs),
195
+ },
196
+ };
197
+ case "Ticker":
198
+ return {
199
+ id: data.id,
200
+ type: data.type,
201
+ position: position,
202
+ arguments: {
203
+ intervalValue: draft(data.arguments.intervalValue),
204
+ intervalUnit: data.arguments.intervalUnit,
205
+ },
206
+ };
207
+ case "Alarm":
208
+ return {
209
+ id: data.id,
210
+ type: data.type,
211
+ position: position,
212
+ arguments: {
213
+ time: data.arguments.time,
214
+ days: data.arguments.days,
215
+ },
216
+ };
217
+ case "WebSearchTool":
218
+ return {
219
+ id: data.id,
220
+ type: data.type,
221
+ position: position,
222
+ arguments: {
223
+ maxResults: data.arguments.maxResults,
224
+ },
225
+ };
226
+ case "Retriever":
227
+ return {
228
+ id: data.id,
229
+ type: data.type,
230
+ position: position,
231
+ arguments: {
232
+ memoryReference: data.arguments.memoryReference,
233
+ topK: draft(data.arguments.topK),
234
+ query: data.arguments.query,
235
+ output: data.arguments.output,
236
+ toolDescription: data.arguments.toolDescription,
237
+ },
238
+ };
239
+ case "WebFetch":
240
+ return {
241
+ id: data.id,
242
+ type: data.type,
243
+ position: position,
244
+ arguments: {
245
+ url: data.arguments.url,
246
+ maxChars: data.arguments.maxChars,
247
+ output: data.arguments.output,
248
+ },
249
+ };
250
+ case "FunctionCall": {
251
+ // Frontend stores FunctionCall args flat (unified with every other node), but
252
+ // the API schema keeps the nested { inputBindings, outputBindings } shape.
253
+ // Translate here so the api format stays stable. `toolDescription` sits
254
+ // alongside the bindings at the api level and is only emitted when the
255
+ // node is currently wired as a tool (exec-mode calls don't need it).
256
+ const inputBindings: Record<string, Expression> = {};
257
+ const outputBindings: Record<string, OutputBinding> = {};
258
+ const args = data.arguments as Record<string, unknown>;
259
+ for (const arg of data.functionInfo.arguments) {
260
+ const key = arg.uid ?? arg.name;
261
+ const v = args[key];
262
+ if (v !== undefined) inputBindings[key] = v as Expression;
263
+ }
264
+ for (const ret of data.functionInfo.returns) {
265
+ const key = ret.uid ?? ret.name;
266
+ const v = args[key];
267
+ if (v !== undefined) outputBindings[key] = v as OutputBinding;
268
+ }
269
+ const toolDescription = args.toolDescription as string | undefined;
270
+ return {
271
+ id: data.id,
272
+ type: data.type,
273
+ functionId: data.functionInfo.id,
274
+ position: position,
275
+ arguments: {
276
+ inputBindings,
277
+ outputBindings,
278
+ ...(isToolInput && toolDescription !== undefined ? { toolDescription } : {}),
279
+ },
280
+ };
281
+ }
282
+ case "SetVariable":
283
+ return {
284
+ id: data.id,
285
+ type: data.type,
286
+ position: position,
287
+ arguments: {
288
+ variable: draft(data.arguments.variable),
289
+ value: data.arguments.value,
290
+ },
291
+ };
292
+ case "MqttPublish":
293
+ return {
294
+ id: data.id,
295
+ type: data.type,
296
+ position: position,
297
+ arguments: {
298
+ channelReference: data.arguments.channelReference ?? "",
299
+ dataType: data.arguments.dataType,
300
+ value: data.arguments.value,
301
+ // Unset stays unset Number(undefined) is NaN, which stringifies to null.
302
+ qos: draft(data.arguments.qos === undefined ? undefined : (Number(data.arguments.qos) as 0 | 1 | 2)),
303
+ retain: data.arguments.retain,
304
+ },
305
+ };
306
+ case "OnMqttMessage":
307
+ return {
308
+ id: data.id,
309
+ type: data.type,
310
+ position: position,
311
+ arguments: {
312
+ channelReference: data.arguments.channelReference ?? "",
313
+ dataType: data.arguments.dataType,
314
+ output: data.arguments.output,
315
+ },
316
+ };
317
+ }
318
+ }
319
+
320
+ /**
321
+ * Convert a strict API Node to a domain Node (NodeData + position). `resolveFunctionInfo`
322
+ * is required only for `FunctionCall` nodes — see {@link ResolveFunctionInfo}.
323
+ */
324
+ export function deserialize(apiNode: ApiNode, resolveFunctionInfo?: ResolveFunctionInfo): Node {
325
+ return { ...deserializeNodeData(apiNode, resolveFunctionInfo), position: apiNode.position };
326
+ }
327
+
328
+ /** Build the NodeData payload from an API Node (no position). */
329
+ function deserializeNodeData(apiNode: Schemas["Node"], resolveFunctionInfo?: ResolveFunctionInfo): NodeData {
330
+ switch (apiNode.type) {
331
+ case "ReadPin":
332
+ return {
333
+ id: apiNode.id,
334
+ type: apiNode.type,
335
+ label: apiNode.label,
336
+ arguments: {
337
+ pinReference: apiNode.arguments.pinReference ?? "",
338
+ signalType: apiNode.arguments.signalType,
339
+ output: apiNode.arguments.output as OutputBinding,
340
+ toolDescription: apiNode.arguments.toolDescription,
341
+ },
342
+ };
343
+ case "SerialRead":
344
+ return {
345
+ id: apiNode.id,
346
+ type: apiNode.type,
347
+ label: apiNode.label,
348
+ arguments: {
349
+ portReference: apiNode.arguments.portReference ?? "",
350
+ prompt: apiNode.arguments.prompt ?? "",
351
+ output: apiNode.arguments.output as OutputBinding,
352
+ },
353
+ };
354
+ case "Retriever":
355
+ return {
356
+ id: apiNode.id,
357
+ type: apiNode.type,
358
+ label: apiNode.label,
359
+ arguments: {
360
+ memoryReference: apiNode.arguments.memoryReference ?? "",
361
+ topK: apiNode.arguments.topK ?? 0,
362
+ query: apiNode.arguments.query,
363
+ output: apiNode.arguments.output as OutputBinding,
364
+ toolDescription: apiNode.arguments.toolDescription,
365
+ },
366
+ };
367
+ case "WritePin":
368
+ return {
369
+ id: apiNode.id,
370
+ type: apiNode.type,
371
+ label: apiNode.label,
372
+ arguments: {
373
+ pinReference: apiNode.arguments.pinReference ?? "",
374
+ signalType: apiNode.arguments.signalType,
375
+ value: apiNode.arguments.value,
376
+ },
377
+ };
378
+ case "SerialWrite":
379
+ return {
380
+ id: apiNode.id,
381
+ type: apiNode.type,
382
+ label: apiNode.label,
383
+ arguments: {
384
+ portReference: apiNode.arguments.portReference ?? "",
385
+ value: apiNode.arguments.value,
386
+ },
387
+ };
388
+ case "Agent":
389
+ return {
390
+ id: apiNode.id,
391
+ type: apiNode.type,
392
+ label: apiNode.label,
393
+ arguments: {
394
+ name: apiNode.arguments.name ?? "",
395
+ model: apiNode.arguments.model ?? "",
396
+ instructions: apiNode.arguments.instructions ?? "",
397
+ maxTurns: apiNode.arguments.maxTurns,
398
+ outputDeclarations: apiNode.arguments.outputDeclarations as OutputDeclaration[],
399
+ memoryRefs: apiNode.arguments.memoryRefs ?? [],
400
+ answer: apiNode.arguments.answer as OutputBinding,
401
+ toolDescription: apiNode.arguments.toolDescription,
402
+ },
403
+ };
404
+ case "If":
405
+ return {
406
+ id: apiNode.id,
407
+ type: apiNode.type,
408
+ label: apiNode.label,
409
+ arguments: {
410
+ condition: apiNode.arguments.condition,
411
+ },
412
+ };
413
+ case "OnFunctionCall":
414
+ return { id: apiNode.id, type: apiNode.type, label: apiNode.label, arguments: {} };
415
+ case "OnStartup":
416
+ return { id: apiNode.id, type: apiNode.type, label: apiNode.label, arguments: {} };
417
+ case "OnPinEdge":
418
+ return {
419
+ id: apiNode.id,
420
+ type: apiNode.type,
421
+ label: apiNode.label,
422
+ arguments: {
423
+ pinReference: apiNode.arguments.pinReference ?? "",
424
+ edge: apiNode.arguments.edge,
425
+ },
426
+ };
427
+ case "OnSerialReceive":
428
+ return {
429
+ id: apiNode.id,
430
+ type: apiNode.type,
431
+ label: apiNode.label,
432
+ arguments: {
433
+ portReference: apiNode.arguments.portReference ?? "",
434
+ output: apiNode.arguments.output as OutputBinding,
435
+ },
436
+ };
437
+ case "OnThreshold":
438
+ return {
439
+ id: apiNode.id,
440
+ type: apiNode.type,
441
+ label: apiNode.label,
442
+ arguments: {
443
+ variable: apiNode.arguments.variable,
444
+ threshold: apiNode.arguments.threshold,
445
+ direction: apiNode.arguments.direction,
446
+ deadband: apiNode.arguments.deadband,
447
+ output: apiNode.arguments.output as OutputBinding,
448
+ },
449
+ };
450
+ case "Delay":
451
+ return {
452
+ id: apiNode.id,
453
+ type: apiNode.type,
454
+ label: apiNode.label,
455
+ arguments: {
456
+ delayMs: apiNode.arguments.delayMs ?? 0,
457
+ },
458
+ };
459
+ case "Ticker":
460
+ return {
461
+ id: apiNode.id,
462
+ type: apiNode.type,
463
+ label: apiNode.label,
464
+ arguments: {
465
+ intervalValue: apiNode.arguments.intervalValue ?? 0,
466
+ intervalUnit: apiNode.arguments.intervalUnit,
467
+ },
468
+ };
469
+ case "Alarm":
470
+ return {
471
+ id: apiNode.id,
472
+ type: apiNode.type,
473
+ label: apiNode.label,
474
+ arguments: {
475
+ time: apiNode.arguments.time ?? "",
476
+ days: apiNode.arguments.days,
477
+ },
478
+ };
479
+ case "WebSearchTool":
480
+ return {
481
+ id: apiNode.id,
482
+ type: apiNode.type,
483
+ label: apiNode.label,
484
+ arguments: {
485
+ maxResults: apiNode.arguments.maxResults,
486
+ },
487
+ };
488
+ case "WebFetch":
489
+ return {
490
+ id: apiNode.id,
491
+ type: apiNode.type,
492
+ label: apiNode.label,
493
+ arguments: {
494
+ url: apiNode.arguments.url,
495
+ maxChars: apiNode.arguments.maxChars,
496
+ output: apiNode.arguments.output as OutputBinding,
497
+ },
498
+ };
499
+ case "SetVariable":
500
+ return {
501
+ id: apiNode.id,
502
+ type: apiNode.type,
503
+ label: apiNode.label,
504
+ arguments: {
505
+ variable: apiNode.arguments.variable,
506
+ value: apiNode.arguments.value,
507
+ },
508
+ };
509
+ case "FunctionCall": {
510
+ // Lift the api's nested { inputBindings, outputBindings } into the flat
511
+ // domain arguments record. Uid collisions are impossible within a single
512
+ // function (one namespace across args + returns). `toolDescription`
513
+ // sits at the same level in the api and is folded into the flat bag
514
+ // under the reserved `toolDescription` key.
515
+ const flat: Record<string, Expression | OutputBinding | string> = {
516
+ ...((apiNode.arguments.inputBindings ?? {}) as Record<string, Expression>),
517
+ ...((apiNode.arguments.outputBindings ?? {}) as Record<string, OutputBinding>),
518
+ };
519
+ if (apiNode.arguments.toolDescription !== undefined) {
520
+ flat.toolDescription = apiNode.arguments.toolDescription;
521
+ }
522
+ // The wire carries only `functionId`; rebuild the in-memory signature snapshot
523
+ // from the workflow's function table. A missing function (deleted/hand-edited)
524
+ // gets a minimal stub so the node still loads and surfaces as deleted.
525
+ const functionInfo: FunctionInfo = resolveFunctionInfo?.(apiNode.functionId) ?? {
526
+ id: apiNode.functionId,
527
+ version: 0,
528
+ name: "",
529
+ arguments: [],
530
+ returns: [],
531
+ };
532
+ return {
533
+ id: apiNode.id,
534
+ type: apiNode.type,
535
+ label: apiNode.label,
536
+ functionInfo,
537
+ arguments: flat,
538
+ };
539
+ }
540
+ case "MqttPublish":
541
+ return {
542
+ id: apiNode.id,
543
+ type: apiNode.type,
544
+ label: apiNode.label,
545
+ arguments: {
546
+ channelReference: apiNode.arguments.channelReference ?? "",
547
+ dataType: apiNode.arguments.dataType,
548
+ value: apiNode.arguments.value,
549
+ qos: String(apiNode.arguments.qos) as "0" | "1" | "2",
550
+ retain: apiNode.arguments.retain,
551
+ },
552
+ };
553
+ case "OnMqttMessage":
554
+ return {
555
+ id: apiNode.id,
556
+ type: apiNode.type,
557
+ label: apiNode.label,
558
+ arguments: {
559
+ channelReference: apiNode.arguments.channelReference ?? "",
560
+ dataType: apiNode.arguments.dataType,
561
+ output: apiNode.arguments.output as OutputBinding,
562
+ },
563
+ };
564
+ }
565
+ }