@foresthubai/workflow-core 0.4.6 → 0.4.8

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 (114) hide show
  1. package/dist/api/camera.d.ts +254 -0
  2. package/dist/api/camera.d.ts.map +1 -0
  3. package/dist/api/camera.js +6 -0
  4. package/dist/api/camera.js.map +1 -0
  5. package/dist/api/engine.d.ts +89 -10
  6. package/dist/api/engine.d.ts.map +1 -1
  7. package/dist/api/index.d.ts +0 -4
  8. package/dist/api/index.d.ts.map +1 -1
  9. package/dist/api/workflow.d.ts +61 -4
  10. package/dist/api/workflow.d.ts.map +1 -1
  11. package/dist/channel/Channel.d.ts +1 -1
  12. package/dist/channel/Channel.d.ts.map +1 -1
  13. package/dist/channel/Channel.js +1 -1
  14. package/dist/channel/Channel.js.map +1 -1
  15. package/dist/channel/ChannelDefinition.d.ts.map +1 -1
  16. package/dist/channel/ChannelDefinition.js +16 -0
  17. package/dist/channel/ChannelDefinition.js.map +1 -1
  18. package/dist/channel/serialization.d.ts.map +1 -1
  19. package/dist/channel/serialization.js +6 -0
  20. package/dist/channel/serialization.js.map +1 -1
  21. package/dist/deploy/bindingRequirements.d.ts +4 -0
  22. package/dist/deploy/bindingRequirements.d.ts.map +1 -0
  23. package/dist/deploy/bindingRequirements.js +59 -0
  24. package/dist/deploy/bindingRequirements.js.map +1 -0
  25. package/dist/deploy/constants.d.ts +8 -0
  26. package/dist/deploy/constants.d.ts.map +1 -0
  27. package/dist/deploy/constants.js +26 -0
  28. package/dist/deploy/constants.js.map +1 -0
  29. package/dist/deploy/index.d.ts +3 -5
  30. package/dist/deploy/index.d.ts.map +1 -1
  31. package/dist/deploy/index.js +10 -2
  32. package/dist/deploy/index.js.map +1 -1
  33. package/dist/deploy/inputs.d.ts +30 -3
  34. package/dist/deploy/inputs.d.ts.map +1 -1
  35. package/dist/deploy/requirements.d.ts +4 -36
  36. package/dist/deploy/requirements.d.ts.map +1 -1
  37. package/dist/deploy/requirements.js +64 -76
  38. package/dist/deploy/requirements.js.map +1 -1
  39. package/dist/deploy/spec.d.ts +10 -4
  40. package/dist/deploy/spec.d.ts.map +1 -1
  41. package/dist/deploy/spec.js +233 -63
  42. package/dist/deploy/spec.js.map +1 -1
  43. package/dist/expression/parser.js +10 -1
  44. package/dist/expression/parser.js.map +1 -1
  45. package/dist/model/MLModelDefinition.d.ts +8 -0
  46. package/dist/model/MLModelDefinition.d.ts.map +1 -0
  47. package/dist/model/MLModelDefinition.js +14 -0
  48. package/dist/model/MLModelDefinition.js.map +1 -0
  49. package/dist/model/Model.d.ts +2 -1
  50. package/dist/model/Model.d.ts.map +1 -1
  51. package/dist/model/Model.js +1 -1
  52. package/dist/model/Model.js.map +1 -1
  53. package/dist/model/ModelRegistry.d.ts.map +1 -1
  54. package/dist/model/ModelRegistry.js +2 -0
  55. package/dist/model/ModelRegistry.js.map +1 -1
  56. package/dist/model/index.d.ts +1 -0
  57. package/dist/model/index.d.ts.map +1 -1
  58. package/dist/model/index.js +1 -0
  59. package/dist/model/index.js.map +1 -1
  60. package/dist/model/serialization.d.ts.map +1 -1
  61. package/dist/model/serialization.js +5 -0
  62. package/dist/model/serialization.js.map +1 -1
  63. package/dist/node/AINode.d.ts +31 -0
  64. package/dist/node/AINode.d.ts.map +1 -0
  65. package/dist/node/AINode.js +85 -0
  66. package/dist/node/AINode.js.map +1 -0
  67. package/dist/node/InputNode.d.ts +10 -2
  68. package/dist/node/InputNode.d.ts.map +1 -1
  69. package/dist/node/InputNode.js +16 -0
  70. package/dist/node/InputNode.js.map +1 -1
  71. package/dist/node/MLInferenceNode.d.ts +15 -0
  72. package/dist/node/MLInferenceNode.d.ts.map +1 -0
  73. package/dist/node/MLInferenceNode.js +26 -0
  74. package/dist/node/MLInferenceNode.js.map +1 -0
  75. package/dist/node/Node.d.ts +3 -3
  76. package/dist/node/Node.d.ts.map +1 -1
  77. package/dist/node/NodeRegistry.d.ts.map +1 -1
  78. package/dist/node/NodeRegistry.js +4 -2
  79. package/dist/node/NodeRegistry.js.map +1 -1
  80. package/dist/node/methods.d.ts.map +1 -1
  81. package/dist/node/methods.js +6 -0
  82. package/dist/node/methods.js.map +1 -1
  83. package/dist/node/serialization.d.ts.map +1 -1
  84. package/dist/node/serialization.js +42 -0
  85. package/dist/node/serialization.js.map +1 -1
  86. package/package.json +2 -2
  87. package/src/api/index.ts +0 -15
  88. package/src/api/workflow.ts +61 -4
  89. package/src/channel/Channel.ts +2 -2
  90. package/src/channel/ChannelDefinition.ts +16 -0
  91. package/src/channel/serialization.ts +6 -0
  92. package/src/deploy/constants.ts +30 -0
  93. package/src/deploy/index.ts +14 -17
  94. package/src/deploy/requirements.test.ts +165 -64
  95. package/src/deploy/requirements.ts +122 -159
  96. package/src/diagnostics/diagnostics.test.ts +41 -23
  97. package/src/expression/parser.ts +9 -1
  98. package/src/model/MLModelDefinition.ts +16 -0
  99. package/src/model/Model.ts +6 -2
  100. package/src/model/ModelRegistry.ts +2 -0
  101. package/src/model/index.ts +1 -0
  102. package/src/model/serialization.ts +5 -0
  103. package/src/node/{AgentNode.ts → AINode.ts} +124 -85
  104. package/src/node/InputNode.ts +28 -2
  105. package/src/node/Node.ts +3 -3
  106. package/src/node/NodeRegistry.ts +4 -2
  107. package/src/node/methods.ts +6 -0
  108. package/src/node/serialization.ts +44 -2
  109. package/src/workflow/serialization.test.ts +53 -0
  110. package/src/api/deployment.ts +0 -74
  111. package/src/api/engine.ts +0 -744
  112. package/src/deploy/inputs.ts +0 -44
  113. package/src/deploy/spec.test.ts +0 -227
  114. package/src/deploy/spec.ts +0 -397
@@ -8,7 +8,7 @@ import type { FunctionDeclaration } from "../function";
8
8
  import { NodeCategory } from "../node";
9
9
  import { SetVariableNodeDefinition } from "../node/DataNode";
10
10
  import { RetrieverNodeDefinition } from "../node/InputNode";
11
- import { AgentNodeDefinition } from "../node/AgentNode";
11
+ import { AgentNodeDefinition } from "../node/AINode";
12
12
  import { OnThresholdNodeDefinition } from "../node/TriggerNode";
13
13
  import { WebSearchToolNodeDefinition } from "../node/ToolNode";
14
14
  import {
@@ -101,7 +101,7 @@ describe("computeNodeDiagnostics — expression validation", () => {
101
101
  const exprDef = makeNodeDef({
102
102
  category: NodeCategory.Input,
103
103
  parameters: [
104
- { id: "val", label: "Value", description: "", type: "expression", expressionType: "int" },
104
+ { id: "val", label: "Value", description: "", type: "expression", expressionType: "int", default: makeExpression("0", "int") },
105
105
  ],
106
106
  });
107
107
 
@@ -114,8 +114,8 @@ describe("computeNodeDiagnostics — expression validation", () => {
114
114
  });
115
115
  const exprDiags = diagsOfCategory(diags, "invalid-expression");
116
116
  expect(exprDiags).toHaveLength(1);
117
- expect(exprDiags[0].paramId).toBe("val");
118
- expect(exprDiags[0].message).toContain("Invalid expression");
117
+ expect(exprDiags[0]?.paramId).toBe("val");
118
+ expect(exprDiags[0]?.message).toContain("Invalid expression");
119
119
  });
120
120
 
121
121
  it("accepts a well-formed expression matching declared type", () => {
@@ -138,7 +138,7 @@ describe("computeNodeDiagnostics — expression validation", () => {
138
138
  });
139
139
  const exprDiags = diagsOfCategory(diags, "invalid-expression");
140
140
  expect(exprDiags).toHaveLength(1);
141
- expect(exprDiags[0].message).toMatch(/Type mismatch|expected/i);
141
+ expect(exprDiags[0]?.message).toMatch(/Type mismatch|expected/i);
142
142
  });
143
143
 
144
144
  it("reactivity: changing a referenced variable's type invalidates a previously-valid expression", () => {
@@ -171,7 +171,24 @@ describe("computeNodeDiagnostics — expression validation", () => {
171
171
  expect(diagsOfCategory(before, "invalid-expression")).toHaveLength(0);
172
172
  const afterExprDiags = diagsOfCategory(after, "invalid-expression");
173
173
  expect(afterExprDiags).toHaveLength(1);
174
- expect(afterExprDiags[0].paramId).toBe("val");
174
+ expect(afterExprDiags[0]?.paramId).toBe("val");
175
+ });
176
+
177
+ it("flags an image variable used in a string expression", () => {
178
+ // A CameraCapture frame (image) interpolated into a text expression has no
179
+ // string form; it must be rejected, not silently emptied at runtime.
180
+ const stringDef = makeNodeDef({
181
+ category: NodeCategory.Input,
182
+ parameters: [{ id: "val", label: "Value", description: "", type: "expression", expressionType: "string", default: makeExpression("0", "string") }],
183
+ });
184
+ const diags = computeNodeDiagnostics({
185
+ ...baseOpts({
186
+ nodeDefinition: stringDef,
187
+ nodeData: makeNode("OnStartup", { val: makeExpression("frame is ${}", "string", [makeDeclaredRef("v1")]) }),
188
+ }),
189
+ availableVariables: makeAvailableVars([makeDeclaredVar({ uid: "v1", dataType: "image" })]),
190
+ });
191
+ expect(diagsOfCategory(diags, "invalid-expression")).toHaveLength(1);
175
192
  });
176
193
 
177
194
  it("skips validation on inactive expression parameters", () => {
@@ -185,6 +202,7 @@ describe("computeNodeDiagnostics — expression validation", () => {
185
202
  description: "",
186
203
  type: "expression",
187
204
  expressionType: "int",
205
+ default: makeExpression("0", "int"),
188
206
  activationRules: [{ type: "parameterIn", parameterId: "mode", values: ["on"] }],
189
207
  },
190
208
  ],
@@ -219,13 +237,13 @@ describe("computeNodeDiagnostics — missing required parameters", () => {
219
237
  });
220
238
  const missing = diagsOfCategory(diags, "missing-required-param");
221
239
  expect(missing).toHaveLength(1);
222
- expect(missing[0].paramId).toBe("name");
240
+ expect(missing[0]?.paramId).toBe("name");
223
241
  });
224
242
 
225
243
  it("flags required expression param that is empty", () => {
226
244
  const def = makeNodeDef({
227
245
  category: NodeCategory.Input,
228
- parameters: [{ id: "expr", label: "Expr", description: "", type: "expression", expressionType: "int" }],
246
+ parameters: [{ id: "expr", label: "Expr", description: "", type: "expression", expressionType: "int", default: makeExpression("0", "int") }],
229
247
  });
230
248
  const diags = computeNodeDiagnostics({
231
249
  ...baseOpts({
@@ -319,8 +337,8 @@ describe("computeNodeDiagnostics — variableSelect validation", () => {
319
337
  });
320
338
  const refDiags = diagsOfCategory(diags, "invalid-reference");
321
339
  expect(refDiags).toHaveLength(1);
322
- expect(refDiags[0].message).toMatch(/deleted variable/);
323
- expect(refDiags[0].paramId).toBe("ref");
340
+ expect(refDiags[0]?.message).toMatch(/deleted variable/);
341
+ expect(refDiags[0]?.paramId).toBe("ref");
324
342
  });
325
343
 
326
344
  it("accepts a valid variable reference", () => {
@@ -369,7 +387,7 @@ describe("computeNodeDiagnostics — channelSelect validation", () => {
369
387
  });
370
388
  const refDiags = diagsOfCategory(diags, "invalid-reference");
371
389
  expect(refDiags).toHaveLength(1);
372
- expect(refDiags[0].message).toMatch(/deleted channel/);
390
+ expect(refDiags[0]?.message).toMatch(/deleted channel/);
373
391
  });
374
392
 
375
393
  it("flags reference to a channel with incompatible type", () => {
@@ -382,7 +400,7 @@ describe("computeNodeDiagnostics — channelSelect validation", () => {
382
400
  });
383
401
  const refDiags = diagsOfCategory(diags, "invalid-reference");
384
402
  expect(refDiags).toHaveLength(1);
385
- expect(refDiags[0].message).toMatch(/not a compatible channel type/);
403
+ expect(refDiags[0]?.message).toMatch(/not a compatible channel type/);
386
404
  });
387
405
 
388
406
  it("accepts reference to a channel with compatible type", () => {
@@ -432,7 +450,7 @@ describe("computeNodeDiagnostics — memorySelect validation", () => {
432
450
  });
433
451
  const refDiags = diagsOfCategory(diags, "invalid-reference");
434
452
  expect(refDiags).toHaveLength(1);
435
- expect(refDiags[0].message).toMatch(/deleted memory/);
453
+ expect(refDiags[0]?.message).toMatch(/deleted memory/);
436
454
  });
437
455
 
438
456
  it("accepts a memory id present in the store with a compatible type", () => {
@@ -456,7 +474,7 @@ describe("computeNodeDiagnostics — memorySelect validation", () => {
456
474
  });
457
475
  const refDiags = diagsOfCategory(diags, "invalid-reference");
458
476
  expect(refDiags).toHaveLength(1);
459
- expect(refDiags[0].message).toMatch(/not a compatible memory type/);
477
+ expect(refDiags[0]?.message).toMatch(/not a compatible memory type/);
460
478
  });
461
479
 
462
480
  it("does not flag when the memory map is undefined (not provided)", () => {
@@ -500,14 +518,14 @@ describe("computeNodeDiagnostics — scalar output binding validation", () => {
500
518
  const diags = computeNodeDiagnostics(retrieverBase({ active: true, mode: "assign", target: { srcId: "", varId: "v1" } }));
501
519
  const assignDiags = diagsOfCategory(diags, "assign-type-mismatch");
502
520
  expect(assignDiags).toHaveLength(1);
503
- expect(assignDiags[0].message).toMatch(/no variable selected/);
521
+ expect(assignDiags[0]?.message).toMatch(/no variable selected/);
504
522
  });
505
523
 
506
524
  it("flags assign-mode binding targeting a deleted variable", () => {
507
525
  const diags = computeNodeDiagnostics(retrieverBase({ active: true, mode: "assign", target: makeDeclaredRef("ghost") }));
508
526
  const assignDiags = diagsOfCategory(diags, "assign-type-mismatch");
509
527
  expect(assignDiags).toHaveLength(1);
510
- expect(assignDiags[0].message).toMatch(/deleted variable/);
528
+ expect(assignDiags[0]?.message).toMatch(/deleted variable/);
511
529
  });
512
530
 
513
531
  it("flags assign-mode binding with type mismatch", () => {
@@ -519,7 +537,7 @@ describe("computeNodeDiagnostics — scalar output binding validation", () => {
519
537
  );
520
538
  const assignDiags = diagsOfCategory(diags, "assign-type-mismatch");
521
539
  expect(assignDiags).toHaveLength(1);
522
- expect(assignDiags[0].message).toMatch(/cannot assign/);
540
+ expect(assignDiags[0]?.message).toMatch(/cannot assign/);
523
541
  });
524
542
 
525
543
  it("accepts assign-mode binding with matching types", () => {
@@ -579,8 +597,8 @@ describe("computeNodeDiagnostics — list-output entry validation", () => {
579
597
  );
580
598
  const entry = diagsOfCategory(diags, "assign-type-mismatch");
581
599
  expect(entry).toHaveLength(1);
582
- expect(entry[0].outputId).toBe("outputDeclarations[0]");
583
- expect(entry[0].message).toMatch(/entry #1 has no variable selected/);
600
+ expect(entry[0]?.outputId).toBe("outputDeclarations[0]");
601
+ expect(entry[0]?.message).toMatch(/entry #1 has no variable selected/);
584
602
  });
585
603
 
586
604
  it("flags list entry targeting a deleted variable", () => {
@@ -589,7 +607,7 @@ describe("computeNodeDiagnostics — list-output entry validation", () => {
589
607
  );
590
608
  const entry = diagsOfCategory(diags, "assign-type-mismatch");
591
609
  expect(entry).toHaveLength(1);
592
- expect(entry[0].message).toMatch(/deleted variable/);
610
+ expect(entry[0]?.message).toMatch(/deleted variable/);
593
611
  });
594
612
 
595
613
  it("flags list entry with type mismatch", () => {
@@ -601,7 +619,7 @@ describe("computeNodeDiagnostics — list-output entry validation", () => {
601
619
  );
602
620
  const entry = diagsOfCategory(diags, "assign-type-mismatch");
603
621
  expect(entry).toHaveLength(1);
604
- expect(entry[0].message).toMatch(/cannot assign/);
622
+ expect(entry[0]?.message).toMatch(/cannot assign/);
605
623
  });
606
624
 
607
625
  it("accepts a valid list entry", () => {
@@ -628,7 +646,7 @@ describe("computeNodeDiagnostics — list-output entry validation", () => {
628
646
  );
629
647
  const entry = diagsOfCategory(diags, "assign-type-mismatch");
630
648
  expect(entry).toHaveLength(1);
631
- expect(entry[0].outputId).toBe("outputDeclarations[1]");
649
+ expect(entry[0]?.outputId).toBe("outputDeclarations[1]");
632
650
  });
633
651
 
634
652
  it("flags duplicate names across the list (both modes)", () => {
@@ -654,7 +672,7 @@ describe("computeNodeDiagnostics — list-output entry validation", () => {
654
672
  );
655
673
  const missing = diagsOfCategory(diags, "missing-required-param").filter((d) => d.outputId);
656
674
  expect(missing).toHaveLength(1);
657
- expect(missing[0].message).toMatch(/has no name/);
675
+ expect(missing[0]?.message).toMatch(/has no name/);
658
676
  });
659
677
  });
660
678
 
@@ -187,8 +187,13 @@ function inferBinaryType(node: jsep.BinaryExpression, ctx: TypeContext, errors:
187
187
  // If either operand failed to type, propagate null
188
188
  if (!leftType || !rightType) return null;
189
189
 
190
- // String concatenation: string + any → string
190
+ // String concatenation: string + any → string. An image has no text form,
191
+ // so it cannot be concatenated even though the other operand is a string.
191
192
  if (op === "+" && (leftType === "string" || rightType === "string")) {
193
+ if (leftType === "image" || rightType === "image") {
194
+ errors.push("An image value cannot be used in a string expression");
195
+ return null;
196
+ }
192
197
  return "string";
193
198
  }
194
199
 
@@ -338,6 +343,9 @@ function isNumeric(type: DataType | null): boolean {
338
343
  function isCompatible(from: DataType, to: DataType): boolean {
339
344
  if (from === to) return true;
340
345
  if (isNumeric(from) && isNumeric(to)) return true;
346
+ // An image is opaque binary with no text form; it is only compatible with an
347
+ // image sink (handled by from === to above), never the string catch-all.
348
+ if (from === "image") return false;
341
349
  if (to === "string") return true;
342
350
  return false;
343
351
  }
@@ -0,0 +1,16 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) 2026 ForestHub.
3
+
4
+ import type { ModelDefinition } from "./ModelDefinition";
5
+
6
+ /**
7
+ * A declared machine-learning model, served by an inference component, that nodes
8
+ * can reference. The component selects the model by id and derives its behaviour
9
+ * from the mounted model bundle, so nothing needs to be configured here.
10
+ */
11
+ export const MLModelDefinition: ModelDefinition = {
12
+ type: "MLModel",
13
+ label: "ML Model",
14
+ description: "A machine-learning model served by an inference component (e.g. object detection)",
15
+ parameters: [],
16
+ };
@@ -20,9 +20,9 @@ import type { Schemas } from "../api";
20
20
  /** A capability a model supports (chat, embedding, vision, ...). From the api. */
21
21
  export type ModelCapability = Schemas["ModelCapability"];
22
22
 
23
- export type ModelType = "LLMModel";
23
+ export type ModelType = "LLMModel" | "MLModel";
24
24
 
25
- export const ALL_MODEL_TYPES: ModelType[] = ["LLMModel"];
25
+ export const ALL_MODEL_TYPES: ModelType[] = ["LLMModel", "MLModel"];
26
26
 
27
27
  export interface Model {
28
28
  id: string;
@@ -39,4 +39,8 @@ export interface ModelInfo {
39
39
  id: string;
40
40
  label: string;
41
41
  capabilities: ModelCapability[];
42
+ // Catalog provider that serves this model (e.g. "anthropic"). The deploy
43
+ // resolver reads it to emit one ExternalResources provider entry per distinct
44
+ // provider a workflow's Agent nodes reference. Mirrors llmproxy ModelInfo.provider.
45
+ provider: string;
42
46
  }
@@ -4,6 +4,7 @@
4
4
  import type { ModelType } from "./Model";
5
5
  import type { ModelDefinition } from "./ModelDefinition";
6
6
  import { LLMModelDefinition } from "./LLMModelDefinition";
7
+ import { MLModelDefinition } from "./MLModelDefinition";
7
8
 
8
9
  /**
9
10
  * Central registry for declared (custom) model variant definitions (one per
@@ -16,6 +17,7 @@ class ModelDefinitionRegistry {
16
17
  initialize() {
17
18
  if (this.initialized) return;
18
19
  this.register(LLMModelDefinition);
20
+ this.register(MLModelDefinition);
19
21
  this.initialized = true;
20
22
  }
21
23
 
@@ -5,6 +5,7 @@ export type { ModelType, Model, ModelCapability, ModelInfo } from "./Model";
5
5
  export { ALL_MODEL_TYPES } from "./Model";
6
6
  export type { ModelDefinition } from "./ModelDefinition";
7
7
  export { LLMModelDefinition } from "./LLMModelDefinition";
8
+ export { MLModelDefinition } from "./MLModelDefinition";
8
9
  export { ModelRegistry } from "./ModelRegistry";
9
10
  export { serialize, deserialize } from "./serialization";
10
11
  export type { ApiModel } from "./serialization";
@@ -17,6 +17,8 @@ export function serialize(model: Model): ApiModel {
17
17
  label,
18
18
  capabilities: (args.capabilities as ModelCapability[]) ?? ["chat"],
19
19
  };
20
+ case "MLModel":
21
+ return { type, id, label };
20
22
  }
21
23
  }
22
24
 
@@ -28,6 +30,9 @@ export function deserialize(api: ApiModel): Model {
28
30
  case "LLMModel":
29
31
  args.capabilities = api.capabilities;
30
32
  break;
33
+ case "MLModel":
34
+ // No type-specific arguments; the component selects the model by id.
35
+ break;
31
36
  }
32
37
  return { id, label, type, arguments: args };
33
38
  }
@@ -1,85 +1,124 @@
1
- // SPDX-License-Identifier: Apache-2.0
2
- // Copyright (c) 2026 ForestHub.
3
-
4
- import { NodeBase } from "./Node";
5
- import { OutputBinding, OutputDeclaration } from "../parameter";
6
- import type { Schemas } from "../api";
7
- import { NodeCategory } from "./constants";
8
- import { NodeDefinition } from "./NodeDefinition";
9
-
10
- export type MemoryRef = Schemas["MemoryRef"];
11
-
12
- export interface AgentNode extends NodeBase {
13
- type: "Agent";
14
- arguments: {
15
- name: string;
16
- model: string;
17
- instructions: string;
18
- maxTurns: number | undefined;
19
- outputDeclarations: OutputDeclaration[];
20
- memoryRefs: MemoryRef[];
21
- answer: OutputBinding;
22
- toolDescription?: string;
23
- };
24
- }
25
-
26
- export type AgentNodeType = "Agent";
27
-
28
- export const AgentNodeDefinition: NodeDefinition = {
29
- type: "Agent",
30
- label: "LLM Agent",
31
- category: NodeCategory.AI,
32
- description: "AI-powered agent for intelligent processing",
33
- canBranch: true, // Control output may fan out to multiple branches (tool output is always multi-target).
34
- outputs: [
35
- { id: "answer", label: "Answer", type: "static", dataType: "string" },
36
- { id: "outputDeclarations", label: "Structured Output", type: "list" },
37
- ],
38
- parameters: [
39
- {
40
- id: "name",
41
- label: "Name",
42
- description: "Name of the agent",
43
- optional: true,
44
- type: "string",
45
- },
46
- {
47
- id: "model",
48
- label: "Model",
49
- description: "AI model to use for the agent",
50
- type: "modelSelect",
51
- modelType: ["LLMModel"],
52
- capabilities: ["chat"],
53
- },
54
- {
55
- id: "instructions",
56
- label: "Instructions",
57
- description: "Instructions for the cloud agent that act as system prompt",
58
- type: "string",
59
- multiline: true,
60
- optional: true,
61
- },
62
- {
63
- id: "maxTurns",
64
- label: "Max Turns",
65
- description: "Maximum number of agent runner turns",
66
- type: "int",
67
- optional: true,
68
- },
69
- {
70
- id: "memoryRefs",
71
- label: "Memory Files",
72
- description: "Project memory files this agent can access, with per-file read or read+write mode",
73
- type: "memory-refs",
74
- default: [],
75
- },
76
- {
77
- id: "toolDescription",
78
- label: "Tool Description",
79
- description: "Description shown to the calling agent when this agent is wired as a tool",
80
- type: "string",
81
- multiline: true,
82
- activationRules: [{ type: "isToolInput" }],
83
- },
84
- ],
85
- };
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) 2026 ForestHub.
3
+
4
+ import { NodeBase } from "./Node";
5
+ import type { Reference, Schemas } from "../api";
6
+ import { OutputBinding, OutputDeclaration } from "../parameter";
7
+ import { NodeCategory } from "./constants";
8
+ import { NodeDefinition } from "./NodeDefinition";
9
+
10
+ export type MemoryRef = Schemas["MemoryRef"];
11
+
12
+ export interface AgentNode extends NodeBase {
13
+ type: "Agent";
14
+ arguments: {
15
+ name: string;
16
+ model: string;
17
+ instructions: string;
18
+ maxTurns: number | undefined;
19
+ outputDeclarations: OutputDeclaration[];
20
+ memoryRefs: MemoryRef[];
21
+ answer: OutputBinding;
22
+ toolDescription?: string;
23
+ };
24
+ }
25
+
26
+ // MLInference runs a declared ML model on an input and returns its result.
27
+ // The model is selected from the workflow's declared MLModels; the input is a
28
+ // reference to a variable whose value is fed to the model, and the result is
29
+ // emitted as a string.
30
+ export interface MLInferenceNode extends NodeBase {
31
+ type: "MLInference";
32
+ arguments: {
33
+ model: string;
34
+ input: Reference | undefined;
35
+ output: OutputBinding;
36
+ };
37
+ }
38
+
39
+ export type AINodeType = "Agent" | "MLInference";
40
+ export type AINode = AgentNode | MLInferenceNode;
41
+
42
+ // Node Definitions
43
+
44
+ export const AgentNodeDefinition: NodeDefinition = {
45
+ type: "Agent",
46
+ label: "LLM Agent",
47
+ category: NodeCategory.AI,
48
+ description: "AI-powered agent for intelligent processing",
49
+ canBranch: true, // Control output may fan out to multiple branches (tool output is always multi-target).
50
+ outputs: [
51
+ { id: "answer", label: "Answer", type: "static", dataType: "string" },
52
+ { id: "outputDeclarations", label: "Structured Output", type: "list" },
53
+ ],
54
+ parameters: [
55
+ {
56
+ id: "name",
57
+ label: "Name",
58
+ description: "Name of the agent",
59
+ optional: true,
60
+ type: "string",
61
+ },
62
+ {
63
+ id: "model",
64
+ label: "Model",
65
+ description: "AI model to use for the agent",
66
+ type: "modelSelect",
67
+ modelType: ["LLMModel"],
68
+ capabilities: ["chat"],
69
+ },
70
+ {
71
+ id: "instructions",
72
+ label: "Instructions",
73
+ description: "Instructions for the cloud agent that act as system prompt",
74
+ type: "string",
75
+ multiline: true,
76
+ optional: true,
77
+ },
78
+ {
79
+ id: "maxTurns",
80
+ label: "Max Turns",
81
+ description: "Maximum number of agent runner turns",
82
+ type: "int",
83
+ optional: true,
84
+ },
85
+ {
86
+ id: "memoryRefs",
87
+ label: "Memory Files",
88
+ description: "Project memory files this agent can access, with per-file read or read+write mode",
89
+ type: "memory-refs",
90
+ default: [],
91
+ },
92
+ {
93
+ id: "toolDescription",
94
+ label: "Tool Description",
95
+ description: "Description shown to the calling agent when this agent is wired as a tool",
96
+ type: "string",
97
+ multiline: true,
98
+ activationRules: [{ type: "isToolInput" }],
99
+ },
100
+ ],
101
+ };
102
+
103
+ export const MLInferenceNodeDefinition: NodeDefinition = {
104
+ type: "MLInference",
105
+ label: "ML Inference",
106
+ category: NodeCategory.AI,
107
+ description: "Runs a machine-learning model on an input and returns its result",
108
+ outputs: [{ id: "output", label: "Result", type: "static", dataType: "string" }],
109
+ parameters: [
110
+ {
111
+ id: "model",
112
+ label: "Model",
113
+ description: "ML model to run",
114
+ type: "modelSelect",
115
+ modelType: ["MLModel"],
116
+ },
117
+ {
118
+ id: "input",
119
+ label: "Input",
120
+ description: "Variable whose value is fed to the model",
121
+ type: "variableSelect",
122
+ },
123
+ ],
124
+ };
@@ -48,8 +48,17 @@ export interface WebFetchNode extends NodeBase {
48
48
  };
49
49
  }
50
50
 
51
- export type InputNodeType = "ReadPin" | "SerialRead" | "Retriever" | "WebFetch";
52
- export type InputNode = ReadPinNode | SerialReadNode | RetrieverNode | WebFetchNode;
51
+ // CameraCapture - grabs a single still image from a camera channel
52
+ export interface CameraCaptureNode extends NodeBase {
53
+ type: "CameraCapture";
54
+ arguments: {
55
+ cameraReference: string | undefined;
56
+ output: OutputBinding;
57
+ };
58
+ }
59
+
60
+ export type InputNodeType = "ReadPin" | "SerialRead" | "Retriever" | "WebFetch" | "CameraCapture";
61
+ export type InputNode = ReadPinNode | SerialReadNode | RetrieverNode | WebFetchNode | CameraCaptureNode;
53
62
 
54
63
  // Node Definitions
55
64
 
@@ -186,3 +195,20 @@ export const SerialReadNodeDefinition: NodeDefinition = {
186
195
  },
187
196
  ],
188
197
  };
198
+
199
+ export const CameraCaptureNodeDefinition: NodeDefinition = {
200
+ type: "CameraCapture",
201
+ label: "Camera Capture",
202
+ category: NodeCategory.Input,
203
+ description: "Capture a still image from a camera",
204
+ outputs: [{ id: "output", label: "Image", type: "static", dataType: "image" }],
205
+ parameters: [
206
+ {
207
+ id: "cameraReference",
208
+ label: "Camera",
209
+ description: "Camera to capture from",
210
+ type: "channelSelect",
211
+ channelType: ["CAMERA"],
212
+ },
213
+ ],
214
+ };
package/src/node/Node.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  import type { DataType } from "../api";
13
13
  import { InputNode, InputNodeType } from "./InputNode";
14
14
  import { OutputNode, OutputNodeType } from "./OutputNode";
15
- import { AgentNode, AgentNodeType } from "./AgentNode";
15
+ import { AINode, AINodeType } from "./AINode";
16
16
  import { LogicNode, LogicNodeType } from "./LogicNode";
17
17
  import { DataNode, DataNodeType } from "./DataNode";
18
18
  import { TriggerNode, TriggerNodeType } from "./TriggerNode";
@@ -24,7 +24,7 @@ export type NodeOutput = { name: string; dataType: DataType };
24
24
  export type NodeType =
25
25
  | InputNodeType
26
26
  | OutputNodeType
27
- | AgentNodeType
27
+ | AINodeType
28
28
  | LogicNodeType
29
29
  | DataNodeType
30
30
  | TriggerNodeType
@@ -37,7 +37,7 @@ export type NodeType =
37
37
  * It is a union type of all specific node types, each with their own typed parameters.
38
38
  * Use this type when you need strong typing for a specific node.
39
39
  */
40
- export type NodeData = InputNode | OutputNode | AgentNode | LogicNode | DataNode | TriggerNode | ToolNode | FunctionCallNode | MqttNode;
40
+ export type NodeData = InputNode | OutputNode | AINode | LogicNode | DataNode | TriggerNode | ToolNode | FunctionCallNode | MqttNode;
41
41
 
42
42
  /**
43
43
  * Full domain node entity held on a {@link Canvas}: the {@link NodeData}
@@ -1,8 +1,8 @@
1
1
  // SPDX-License-Identifier: Apache-2.0
2
2
  // Copyright (c) 2026 ForestHub.
3
3
 
4
- import { ReadPinNodeDefinition, SerialReadNodeDefinition, RetrieverNodeDefinition, WebFetchNodeDefinition } from "./InputNode";
5
- import { AgentNodeDefinition } from "./AgentNode";
4
+ import { ReadPinNodeDefinition, SerialReadNodeDefinition, RetrieverNodeDefinition, WebFetchNodeDefinition, CameraCaptureNodeDefinition } from "./InputNode";
5
+ import { AgentNodeDefinition, MLInferenceNodeDefinition } from "./AINode";
6
6
  import {
7
7
  DelayNodeDefinition,
8
8
  TickerNodeDefinition,
@@ -38,6 +38,7 @@ class NodeDefinitionRegistry {
38
38
  this.register(WritePinNodeDefinition);
39
39
  this.register(SerialWriteNodeDefinition);
40
40
  this.register(AgentNodeDefinition);
41
+ this.register(MLInferenceNodeDefinition);
41
42
  this.register(IfNodeDefinition);
42
43
  this.register(SetVariableNodeDefinition);
43
44
  // Register trigger nodes
@@ -54,6 +55,7 @@ class NodeDefinitionRegistry {
54
55
  // Register input tool nodes
55
56
  this.register(RetrieverNodeDefinition);
56
57
  this.register(WebFetchNodeDefinition);
58
+ this.register(CameraCaptureNodeDefinition);
57
59
  // Register MQTT nodes
58
60
  this.register(MqttPublishNodeDefinition);
59
61
  this.register(OnMqttMessageNodeDefinition);
@@ -66,6 +66,7 @@ export function getPorts(node: NodeData): PortDefinitions {
66
66
  case "WritePin":
67
67
  case "SerialWrite":
68
68
  case "MqttPublish":
69
+ case "CameraCapture":
69
70
  return {
70
71
  input: [{ id: "ctrl", type: "control" }],
71
72
  output: [{ id: "ctrl", type: "control" }],
@@ -132,6 +133,11 @@ export function getPorts(node: NodeData): PortDefinitions {
132
133
  input: [{ id: "ctrl", type: "control" }],
133
134
  output: [{ id: "ctrl", type: "control" }],
134
135
  };
136
+ case "MLInference":
137
+ return {
138
+ input: [{ id: "ctrl", type: "control" }],
139
+ output: [{ id: "ctrl", type: "control" }],
140
+ };
135
141
  case "WebSearchTool":
136
142
  return { input: [{ id: "tool", type: "tool", label: "As Tool" }], output: [] };
137
143
  }