@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.
- package/dist/api/camera.d.ts +254 -0
- package/dist/api/camera.d.ts.map +1 -0
- package/dist/api/camera.js +6 -0
- package/dist/api/camera.js.map +1 -0
- package/dist/api/engine.d.ts +89 -10
- package/dist/api/engine.d.ts.map +1 -1
- package/dist/api/index.d.ts +0 -4
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/workflow.d.ts +61 -4
- package/dist/api/workflow.d.ts.map +1 -1
- package/dist/channel/Channel.d.ts +1 -1
- package/dist/channel/Channel.d.ts.map +1 -1
- package/dist/channel/Channel.js +1 -1
- package/dist/channel/Channel.js.map +1 -1
- package/dist/channel/ChannelDefinition.d.ts.map +1 -1
- package/dist/channel/ChannelDefinition.js +16 -0
- package/dist/channel/ChannelDefinition.js.map +1 -1
- package/dist/channel/serialization.d.ts.map +1 -1
- package/dist/channel/serialization.js +6 -0
- package/dist/channel/serialization.js.map +1 -1
- package/dist/deploy/bindingRequirements.d.ts +4 -0
- package/dist/deploy/bindingRequirements.d.ts.map +1 -0
- package/dist/deploy/bindingRequirements.js +59 -0
- package/dist/deploy/bindingRequirements.js.map +1 -0
- package/dist/deploy/constants.d.ts +8 -0
- package/dist/deploy/constants.d.ts.map +1 -0
- package/dist/deploy/constants.js +26 -0
- package/dist/deploy/constants.js.map +1 -0
- package/dist/deploy/index.d.ts +3 -5
- package/dist/deploy/index.d.ts.map +1 -1
- package/dist/deploy/index.js +10 -2
- package/dist/deploy/index.js.map +1 -1
- package/dist/deploy/inputs.d.ts +30 -3
- package/dist/deploy/inputs.d.ts.map +1 -1
- package/dist/deploy/requirements.d.ts +4 -36
- package/dist/deploy/requirements.d.ts.map +1 -1
- package/dist/deploy/requirements.js +64 -76
- package/dist/deploy/requirements.js.map +1 -1
- package/dist/deploy/spec.d.ts +10 -4
- package/dist/deploy/spec.d.ts.map +1 -1
- package/dist/deploy/spec.js +233 -63
- package/dist/deploy/spec.js.map +1 -1
- package/dist/expression/parser.js +10 -1
- package/dist/expression/parser.js.map +1 -1
- package/dist/model/MLModelDefinition.d.ts +8 -0
- package/dist/model/MLModelDefinition.d.ts.map +1 -0
- package/dist/model/MLModelDefinition.js +14 -0
- package/dist/model/MLModelDefinition.js.map +1 -0
- package/dist/model/Model.d.ts +2 -1
- package/dist/model/Model.d.ts.map +1 -1
- package/dist/model/Model.js +1 -1
- package/dist/model/Model.js.map +1 -1
- package/dist/model/ModelRegistry.d.ts.map +1 -1
- package/dist/model/ModelRegistry.js +2 -0
- package/dist/model/ModelRegistry.js.map +1 -1
- package/dist/model/index.d.ts +1 -0
- package/dist/model/index.d.ts.map +1 -1
- package/dist/model/index.js +1 -0
- package/dist/model/index.js.map +1 -1
- package/dist/model/serialization.d.ts.map +1 -1
- package/dist/model/serialization.js +5 -0
- package/dist/model/serialization.js.map +1 -1
- package/dist/node/AINode.d.ts +31 -0
- package/dist/node/AINode.d.ts.map +1 -0
- package/dist/node/AINode.js +85 -0
- package/dist/node/AINode.js.map +1 -0
- package/dist/node/InputNode.d.ts +10 -2
- package/dist/node/InputNode.d.ts.map +1 -1
- package/dist/node/InputNode.js +16 -0
- package/dist/node/InputNode.js.map +1 -1
- package/dist/node/MLInferenceNode.d.ts +15 -0
- package/dist/node/MLInferenceNode.d.ts.map +1 -0
- package/dist/node/MLInferenceNode.js +26 -0
- package/dist/node/MLInferenceNode.js.map +1 -0
- package/dist/node/Node.d.ts +3 -3
- package/dist/node/Node.d.ts.map +1 -1
- package/dist/node/NodeRegistry.d.ts.map +1 -1
- package/dist/node/NodeRegistry.js +4 -2
- package/dist/node/NodeRegistry.js.map +1 -1
- package/dist/node/methods.d.ts.map +1 -1
- package/dist/node/methods.js +6 -0
- package/dist/node/methods.js.map +1 -1
- package/dist/node/serialization.d.ts.map +1 -1
- package/dist/node/serialization.js +42 -0
- package/dist/node/serialization.js.map +1 -1
- package/package.json +2 -2
- package/src/api/index.ts +0 -15
- package/src/api/workflow.ts +61 -4
- package/src/channel/Channel.ts +2 -2
- package/src/channel/ChannelDefinition.ts +16 -0
- package/src/channel/serialization.ts +6 -0
- package/src/deploy/constants.ts +30 -0
- package/src/deploy/index.ts +14 -17
- package/src/deploy/requirements.test.ts +165 -64
- package/src/deploy/requirements.ts +122 -159
- package/src/diagnostics/diagnostics.test.ts +41 -23
- package/src/expression/parser.ts +9 -1
- package/src/model/MLModelDefinition.ts +16 -0
- package/src/model/Model.ts +6 -2
- package/src/model/ModelRegistry.ts +2 -0
- package/src/model/index.ts +1 -0
- package/src/model/serialization.ts +5 -0
- package/src/node/{AgentNode.ts → AINode.ts} +124 -85
- package/src/node/InputNode.ts +28 -2
- package/src/node/Node.ts +3 -3
- package/src/node/NodeRegistry.ts +4 -2
- package/src/node/methods.ts +6 -0
- package/src/node/serialization.ts +44 -2
- package/src/workflow/serialization.test.ts +53 -0
- package/src/api/deployment.ts +0 -74
- package/src/api/engine.ts +0 -744
- package/src/deploy/inputs.ts +0 -44
- package/src/deploy/spec.test.ts +0 -227
- package/src/deploy/spec.ts +0 -397
|
@@ -1,64 +1,165 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// Copyright (c) 2026 ForestHub.
|
|
3
|
-
|
|
4
|
-
import { describe, it, expect } from "vitest";
|
|
5
|
-
import { getReferencedCatalogModelIds } from "./requirements";
|
|
6
|
-
import { MAIN_CANVAS_ID, type Workflow, type Canvas } from "../workflow";
|
|
7
|
-
import type {
|
|
8
|
-
import type { Model } from "../model";
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
});
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) 2026 ForestHub.
|
|
3
|
+
|
|
4
|
+
import { describe, it, expect } from "vitest";
|
|
5
|
+
import { workflowBindingRequirements, getReferencedCatalogModelIds, type BindingKind } from "./requirements";
|
|
6
|
+
import { MAIN_CANVAS_ID, type Workflow, type Canvas } from "../workflow";
|
|
7
|
+
import type { Channel, ChannelType } from "../channel";
|
|
8
|
+
import type { Model } from "../model";
|
|
9
|
+
import type { Node } from "../node";
|
|
10
|
+
|
|
11
|
+
function channel(id: string, type: ChannelType): Channel {
|
|
12
|
+
return { id, label: id, type, arguments: {} };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const llm = (id: string): Model => ({ id, label: id, type: "LLMModel", arguments: {} });
|
|
16
|
+
const ml = (id: string): Model => ({ id, label: id, type: "MLModel", arguments: {} });
|
|
17
|
+
|
|
18
|
+
// Minimal Agent node referencing a catalog model by id — only the model ref
|
|
19
|
+
// matters to the requirement walk. Cast through the union: the other Agent args
|
|
20
|
+
// are irrelevant here.
|
|
21
|
+
function agent(id: string, model: string): Node {
|
|
22
|
+
return {
|
|
23
|
+
id,
|
|
24
|
+
type: "Agent",
|
|
25
|
+
position: { x: 0, y: 0 },
|
|
26
|
+
arguments: {
|
|
27
|
+
name: id,
|
|
28
|
+
model,
|
|
29
|
+
instructions: "",
|
|
30
|
+
outputDeclarations: [],
|
|
31
|
+
memoryRefs: [],
|
|
32
|
+
answer: { active: true, mode: "emit", name: "answer" },
|
|
33
|
+
},
|
|
34
|
+
} as unknown as Node;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const canvas = (nodes: Node[]): Canvas => ({ nodes, edges: [], variables: {} });
|
|
38
|
+
|
|
39
|
+
// Build a workflow from parts. `nodes` fills the main canvas; pass `canvases`
|
|
40
|
+
// instead to model multiple bodies (main + function). Any other field
|
|
41
|
+
// (channels, models) overrides the empty default.
|
|
42
|
+
function workflow(parts: Partial<Workflow> & { nodes?: Node[] }): Workflow {
|
|
43
|
+
const { nodes = [], ...rest } = parts;
|
|
44
|
+
return {
|
|
45
|
+
canvases: { [MAIN_CANVAS_ID]: canvas(nodes) },
|
|
46
|
+
functions: {},
|
|
47
|
+
channels: {},
|
|
48
|
+
memory: {},
|
|
49
|
+
models: {},
|
|
50
|
+
...rest,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const byId = (chs: Channel[]): Record<string, Channel> => Object.fromEntries(chs.map((c) => [c.id, c]));
|
|
55
|
+
const modelsById = (ms: Model[]): Record<string, Model> => Object.fromEntries(ms.map((m) => [m.id, m]));
|
|
56
|
+
|
|
57
|
+
// CROSS-LANGUAGE CONFORMANCE. Each case below is a golden fixture: a workflow in,
|
|
58
|
+
// an exact id->kind surface out. The backend's deploy.WorkflowBindingRequirements
|
|
59
|
+
// must produce the identical map for the same workflow — mirror these fixtures on
|
|
60
|
+
// the Go side so the two extractors stay pinned. Divergence here is deploy drift:
|
|
61
|
+
// OSS and the backend disagreeing about what a workflow needs bound.
|
|
62
|
+
describe("workflowBindingRequirements", () => {
|
|
63
|
+
it("returns an empty surface for an empty workflow", () => {
|
|
64
|
+
expect(workflowBindingRequirements(workflow({}))).toEqual({});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("maps every hardware channel family to 'hardware'", () => {
|
|
68
|
+
const chs = byId([
|
|
69
|
+
channel("in", "GPIOIN"),
|
|
70
|
+
channel("out", "GPIOOUT"),
|
|
71
|
+
channel("adc", "ADC"),
|
|
72
|
+
channel("dac", "DAC"),
|
|
73
|
+
channel("pwm", "PWM"),
|
|
74
|
+
channel("uart", "UART"),
|
|
75
|
+
]);
|
|
76
|
+
const expected: Record<string, BindingKind> = {
|
|
77
|
+
in: "hardware",
|
|
78
|
+
out: "hardware",
|
|
79
|
+
adc: "hardware",
|
|
80
|
+
dac: "hardware",
|
|
81
|
+
pwm: "hardware",
|
|
82
|
+
uart: "hardware",
|
|
83
|
+
};
|
|
84
|
+
expect(workflowBindingRequirements(workflow({ channels: chs }))).toEqual(expected);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("maps MQTT to 'mqtt' and CAMERA to 'camera' (OSS-ahead kind)", () => {
|
|
88
|
+
const chs = byId([channel("telemetry", "MQTT"), channel("cam0", "CAMERA")]);
|
|
89
|
+
expect(workflowBindingRequirements(workflow({ channels: chs }))).toEqual({ telemetry: "mqtt", cam0: "camera" });
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it("binds nothing for a LOG channel", () => {
|
|
93
|
+
const chs = byId([channel("log", "LOG"), channel("led", "GPIOOUT")]);
|
|
94
|
+
expect(workflowBindingRequirements(workflow({ channels: chs }))).toEqual({ led: "hardware" });
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it("splits declared models: LLMModel -> 'declaredModel', MLModel -> 'mlInference'", () => {
|
|
98
|
+
const models = modelsById([llm("local-llm"), ml("yolo")]);
|
|
99
|
+
expect(workflowBindingRequirements(workflow({ models }))).toEqual({ "local-llm": "declaredModel", yolo: "mlInference" });
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("maps a referenced-but-undeclared catalog model to 'catalogModel', keyed by model id", () => {
|
|
103
|
+
const wf = workflow({ nodes: [agent("a1", "gpt-4o")] });
|
|
104
|
+
expect(workflowBindingRequirements(wf)).toEqual({ "gpt-4o": "catalogModel" });
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("prefers 'declaredModel' when an id is both declared and referenced (no overwrite)", () => {
|
|
108
|
+
const wf = workflow({ models: modelsById([llm("shared")]), nodes: [agent("a1", "shared")] });
|
|
109
|
+
expect(workflowBindingRequirements(wf)).toEqual({ shared: "declaredModel" });
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("does not extract MemoryFile / VectorDatabase memory (RAG not yet in the OSS surface)", () => {
|
|
113
|
+
const wf = workflow({
|
|
114
|
+
memory: { notes: { id: "notes", label: "Notes", type: "MemoryFile", arguments: {} } as Workflow["memory"][string] },
|
|
115
|
+
});
|
|
116
|
+
expect(workflowBindingRequirements(wf)).toEqual({});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("produces the full surface for a mixed workflow", () => {
|
|
120
|
+
const wf = workflow({
|
|
121
|
+
channels: byId([channel("led", "GPIOOUT"), channel("telemetry", "MQTT"), channel("cam0", "CAMERA"), channel("log", "LOG")]),
|
|
122
|
+
models: modelsById([llm("local-llm"), ml("yolo")]),
|
|
123
|
+
nodes: [agent("a1", "claude-sonnet")],
|
|
124
|
+
});
|
|
125
|
+
const expected: Record<string, BindingKind> = {
|
|
126
|
+
led: "hardware",
|
|
127
|
+
telemetry: "mqtt",
|
|
128
|
+
cam0: "camera",
|
|
129
|
+
"local-llm": "declaredModel",
|
|
130
|
+
yolo: "mlInference",
|
|
131
|
+
"claude-sonnet": "catalogModel",
|
|
132
|
+
};
|
|
133
|
+
expect(workflowBindingRequirements(wf)).toEqual(expected);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("getReferencedCatalogModelIds", () => {
|
|
138
|
+
it("returns catalog ids (referenced but not declared), excluding declared customs", () => {
|
|
139
|
+
const wf = workflow({
|
|
140
|
+
canvases: { [MAIN_CANVAS_ID]: canvas([agent("n1", "claude-opus-4-7"), agent("n2", "custom-llm")]) },
|
|
141
|
+
models: modelsById([llm("custom-llm")]),
|
|
142
|
+
});
|
|
143
|
+
expect(getReferencedCatalogModelIds(wf)).toEqual(["claude-opus-4-7"]);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("ignores unset model references", () => {
|
|
147
|
+
const wf = workflow({ nodes: [agent("n1", "")] });
|
|
148
|
+
expect(getReferencedCatalogModelIds(wf)).toEqual([]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("dedupes across nodes and walks every canvas (main + function bodies)", () => {
|
|
152
|
+
const wf = workflow({
|
|
153
|
+
canvases: {
|
|
154
|
+
[MAIN_CANVAS_ID]: canvas([agent("n1", "claude-opus-4-7"), agent("n2", "claude-opus-4-7")]),
|
|
155
|
+
fnBody: canvas([agent("n3", "gemini-2")]),
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
expect(getReferencedCatalogModelIds(wf).sort()).toEqual(["claude-opus-4-7", "gemini-2"]);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("returns nothing when every referenced model is declared", () => {
|
|
162
|
+
const wf = workflow({ nodes: [agent("n1", "custom-llm")], models: modelsById([llm("custom-llm")]) });
|
|
163
|
+
expect(getReferencedCatalogModelIds(wf)).toEqual([]);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
@@ -1,159 +1,122 @@
|
|
|
1
|
-
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
-
// Copyright (c) 2026 ForestHub.
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
// The
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
hardwareChannels.push({ id: channel.id, label: channel.label, family: "pwm", addressable: true });
|
|
124
|
-
break;
|
|
125
|
-
case "UART":
|
|
126
|
-
hardwareChannels.push({ id: channel.id, label: channel.label, family: "serial", addressable: false });
|
|
127
|
-
break;
|
|
128
|
-
case "MQTT":
|
|
129
|
-
mqttChannels.push({ id: channel.id, label: channel.label });
|
|
130
|
-
break;
|
|
131
|
-
case "LOG":
|
|
132
|
-
// Resolves to the ambient engine logger — no platform resource to bind, so
|
|
133
|
-
// it demands nothing of the deploy environment.
|
|
134
|
-
break;
|
|
135
|
-
default:
|
|
136
|
-
return assertNeverChannel(channel.type);
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
let hasRetriever = false;
|
|
141
|
-
let hasWebSearch = false;
|
|
142
|
-
for (const canvas of Object.values(workflow.canvases)) {
|
|
143
|
-
for (const node of canvas.nodes) {
|
|
144
|
-
if (node.type === "Retriever") hasRetriever = true;
|
|
145
|
-
else if (node.type === "WebSearchTool") hasWebSearch = true;
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const customModels: CustomModel[] = Object.values(workflow.models).map((m) => ({ id: m.id, label: m.label }));
|
|
150
|
-
|
|
151
|
-
return {
|
|
152
|
-
hasProviderModel: getReferencedCatalogModelIds(workflow).length > 0,
|
|
153
|
-
hasRetriever,
|
|
154
|
-
hasWebSearch,
|
|
155
|
-
hardwareChannels,
|
|
156
|
-
mqttChannels,
|
|
157
|
-
customModels,
|
|
158
|
-
};
|
|
159
|
-
}
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) 2026 ForestHub.
|
|
3
|
+
|
|
4
|
+
// Stage 0 of the deployment pipeline (docs/deployment-pipeline.md): extract the
|
|
5
|
+
// binding SURFACE a workflow demands — which resource ids must be bound, and of
|
|
6
|
+
// what kind — from the workflow content alone. This is the language-neutral
|
|
7
|
+
// requirement analysis every deploy path (OSS CLI, backend) shares.
|
|
8
|
+
|
|
9
|
+
import type { Workflow } from "../workflow";
|
|
10
|
+
|
|
11
|
+
// The binding kind a workflow resource must be bound with — the discriminator of
|
|
12
|
+
// the deployment api's ResourceBindingRequest.
|
|
13
|
+
//
|
|
14
|
+
// CROSS-LANGUAGE SEAM: these string values MUST match the backend's
|
|
15
|
+
// deploy.BindingRequirement constants (fh-backend deploy.WorkflowBindingRequirements),
|
|
16
|
+
// which are themselves the ResourceBindingRequest discriminators
|
|
17
|
+
// ("hardware"/"mqtt"/"camera"/"declaredModel"/"mlInference"/"catalogModel"/"rag").
|
|
18
|
+
// A value that drifts here silently disagrees with the backend about what a
|
|
19
|
+
// workflow needs bound.
|
|
20
|
+
//
|
|
21
|
+
// One deliberate asymmetry with the backend today: "rag" is absent. OSS is behind
|
|
22
|
+
// on retrieval — it does not yet extract a VectorDatabase memory resource as a
|
|
23
|
+
// requirement (the backend emits "rag"). A standalone engine has no retriever, so
|
|
24
|
+
// such a workflow is refused at deploy; add "rag" here when the OSS engine gains a
|
|
25
|
+
// retrieval backing.
|
|
26
|
+
export type BindingKind = "hardware" | "mqtt" | "camera" | "declaredModel" | "mlInference" | "catalogModel";
|
|
27
|
+
|
|
28
|
+
// Drift sentinel: a new ChannelType breaks compilation here until it is
|
|
29
|
+
// classified in workflowBindingRequirements' channel switch.
|
|
30
|
+
function assertNeverChannel(t: never): never {
|
|
31
|
+
throw new Error(`unhandled channel type: ${String(t)}`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Drift sentinel: a new ModelType breaks compilation here until it is classified
|
|
35
|
+
// in workflowBindingRequirements' model switch.
|
|
36
|
+
function assertNeverModel(t: never): never {
|
|
37
|
+
throw new Error(`unhandled model type: ${String(t)}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// workflowBindingRequirements returns the resources a deploy must bind, keyed by
|
|
41
|
+
// workflow logical id — the binding surface. The TS twin of the backend's
|
|
42
|
+
// deploy.WorkflowBindingRequirements: both must derive the SAME id->kind map from
|
|
43
|
+
// the same workflow, or an OSS deploy and a backend deploy disagree about what
|
|
44
|
+
// needs binding.
|
|
45
|
+
//
|
|
46
|
+
// The surface is neither the declared set nor a subset of it:
|
|
47
|
+
// - LOG channels and MemoryFile bind nothing — declared, but out.
|
|
48
|
+
// - referenced catalog models bind a provider yet are node references, not
|
|
49
|
+
// declarations — undeclared, but in.
|
|
50
|
+
//
|
|
51
|
+
// Pure — reads workflow content alone: no operator input, no model catalog.
|
|
52
|
+
export function workflowBindingRequirements(workflow: Workflow): Record<string, BindingKind> {
|
|
53
|
+
const reqs: Record<string, BindingKind> = {};
|
|
54
|
+
|
|
55
|
+
for (const channel of Object.values(workflow.channels)) {
|
|
56
|
+
switch (channel.type) {
|
|
57
|
+
case "GPIOIN":
|
|
58
|
+
case "GPIOOUT":
|
|
59
|
+
case "ADC":
|
|
60
|
+
case "DAC":
|
|
61
|
+
case "PWM":
|
|
62
|
+
case "UART":
|
|
63
|
+
reqs[channel.id] = "hardware";
|
|
64
|
+
break;
|
|
65
|
+
case "MQTT":
|
|
66
|
+
reqs[channel.id] = "mqtt";
|
|
67
|
+
break;
|
|
68
|
+
case "CAMERA":
|
|
69
|
+
reqs[channel.id] = "camera";
|
|
70
|
+
break;
|
|
71
|
+
case "LOG":
|
|
72
|
+
// Resolves to the ambient engine logger — no platform resource to bind.
|
|
73
|
+
break;
|
|
74
|
+
default:
|
|
75
|
+
return assertNeverChannel(channel.type);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Every declared model needs a source binding, but of a different kind by family:
|
|
80
|
+
// an LLMModel binds a model source ("declaredModel"); an MLModel is served by an
|
|
81
|
+
// ml-inference component ("mlInference"). Mirrors the backend's workflowModelIDs
|
|
82
|
+
// split — both sides must agree on which kind each declared model gets.
|
|
83
|
+
for (const model of Object.values(workflow.models)) {
|
|
84
|
+
switch (model.type) {
|
|
85
|
+
case "LLMModel":
|
|
86
|
+
reqs[model.id] = "declaredModel";
|
|
87
|
+
break;
|
|
88
|
+
case "MLModel":
|
|
89
|
+
reqs[model.id] = "mlInference";
|
|
90
|
+
break;
|
|
91
|
+
default:
|
|
92
|
+
return assertNeverModel(model.type);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Catalog models: referenced by an Agent node but not declared. Keyed by model
|
|
97
|
+
// id (canonical, matching the backend surface) — not collapsed to providers
|
|
98
|
+
// here. getReferencedCatalogModelIds already excludes declared ids, so this
|
|
99
|
+
// never overwrites a declaredModel entry.
|
|
100
|
+
for (const id of getReferencedCatalogModelIds(workflow)) reqs[id] = "catalogModel";
|
|
101
|
+
|
|
102
|
+
return reqs;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Model ids that Agent nodes reference but the workflow does not declare in
|
|
107
|
+
* `models` - the catalog models a deploy must bind a provider for.
|
|
108
|
+
*/
|
|
109
|
+
export function getReferencedCatalogModelIds(workflow: Workflow): string[] {
|
|
110
|
+
const declared = new Set(Object.keys(workflow.models));
|
|
111
|
+
const catalogIds = new Set<string>();
|
|
112
|
+
|
|
113
|
+
for (const canvas of Object.values(workflow.canvases)) {
|
|
114
|
+
for (const node of canvas.nodes) {
|
|
115
|
+
if (node.type !== "Agent") continue;
|
|
116
|
+
const id = node.arguments.model;
|
|
117
|
+
if (id !== "" && !declared.has(id)) catalogIds.add(id);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return [...catalogIds];
|
|
122
|
+
}
|