@foresthubai/workflow-core 0.3.0 → 0.4.0
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/LICENSE +202 -202
- package/NOTICE +14 -14
- package/README.md +63 -63
- package/dist/api/workflow.d.ts +2 -2
- package/dist/api/workflow.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/api/index.ts +11 -11
- package/src/api/workflow.ts +607 -607
- package/src/channel/Channel.ts +11 -11
- package/src/channel/ChannelDefinition.ts +76 -76
- package/src/channel/index.ts +6 -6
- package/src/channel/serialization.ts +68 -68
- package/src/deploy/index.ts +1 -1
- package/src/deploy/requirements.test.ts +61 -61
- package/src/deploy/requirements.ts +41 -41
- package/src/diagnostics/__fixtures__/diagnosticFixtures.ts +158 -158
- package/src/diagnostics/diagnostics.test.ts +878 -878
- package/src/diagnostics/diagnostics.ts +936 -936
- package/src/diagnostics/index.ts +11 -11
- package/src/edge/Edge.ts +23 -23
- package/src/edge/EdgeDefinition.ts +45 -45
- package/src/edge/EdgeType.ts +19 -19
- package/src/edge/index.ts +8 -8
- package/src/edge/serialization.ts +83 -83
- package/src/expression/index.ts +4 -4
- package/src/expression/parser.ts +362 -362
- package/src/expression/types.ts +30 -30
- package/src/function/FunctionDeclaration.ts +54 -54
- package/src/function/index.ts +3 -3
- package/src/function/serialization.ts +40 -40
- package/src/globals.d.ts +9 -9
- package/src/id/index.ts +8 -8
- package/src/index.ts +22 -22
- package/src/memory/Memory.ts +15 -15
- package/src/memory/MemoryDefinition.ts +16 -16
- package/src/memory/MemoryFileDefinition.ts +37 -37
- package/src/memory/MemoryRegistry.ts +35 -35
- package/src/memory/VectorDatabaseDefinition.ts +21 -21
- package/src/memory/index.ts +8 -8
- package/src/memory/serialization.ts +47 -47
- package/src/migration/index.ts +4 -4
- package/src/migration/migrate.test.ts +44 -44
- package/src/migration/migrate.ts +58 -58
- package/src/migration/migrations.ts +24 -24
- package/src/migration/version.ts +9 -9
- package/src/model/LLMModelDefinition.ts +12 -12
- package/src/model/Model.ts +39 -39
- package/src/model/ModelDefinition.ts +15 -15
- package/src/model/ModelRegistry.ts +33 -33
- package/src/model/index.ts +7 -7
- package/src/model/serialization.ts +30 -30
- package/src/node/AgentNode.ts +82 -82
- package/src/node/DataNode.ts +41 -41
- package/src/node/FunctionNode.ts +76 -76
- package/src/node/InputNode.ts +185 -185
- package/src/node/LogicNode.ts +33 -33
- package/src/node/MqttNode.ts +127 -127
- package/src/node/Node.ts +61 -61
- package/src/node/NodeDefinition.ts +37 -37
- package/src/node/NodeRegistry.ts +85 -85
- package/src/node/OutputNode.ts +87 -87
- package/src/node/ToolNode.ts +32 -32
- package/src/node/TriggerNode.ts +272 -272
- package/src/node/constants.ts +16 -16
- package/src/node/index.ts +26 -26
- package/src/node/methods.ts +278 -278
- package/src/node/serialization.ts +544 -544
- package/src/parameter/OutputParameter.ts +68 -68
- package/src/parameter/Parameter.ts +243 -243
- package/src/parameter/index.ts +33 -33
- package/src/variable/Variable.ts +10 -10
- package/src/variable/index.ts +16 -16
- package/src/variable/operations.ts +106 -106
- package/src/workflow/Workflow.ts +41 -41
- package/src/workflow/index.ts +3 -3
- package/src/workflow/serialization.test.ts +240 -240
- package/src/workflow/serialization.ts +242 -242
package/src/channel/Channel.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
export type ChannelType = "GPIOIN" | "GPIOOUT" | "ADC" | "PWM" | "DAC" | "UART" | "MQTT";
|
|
2
|
-
|
|
3
|
-
export const ALL_CHANNEL_TYPES: ChannelType[] = ["GPIOIN", "GPIOOUT", "ADC", "PWM", "DAC", "UART", "MQTT"];
|
|
4
|
-
|
|
5
|
-
/** Interface for a channel instance in the workflow */
|
|
6
|
-
export interface Channel {
|
|
7
|
-
id: string;
|
|
8
|
-
label: string;
|
|
9
|
-
type: ChannelType;
|
|
10
|
-
arguments: Record<string, unknown>;
|
|
11
|
-
}
|
|
1
|
+
export type ChannelType = "GPIOIN" | "GPIOOUT" | "ADC" | "PWM" | "DAC" | "UART" | "MQTT";
|
|
2
|
+
|
|
3
|
+
export const ALL_CHANNEL_TYPES: ChannelType[] = ["GPIOIN", "GPIOOUT", "ADC", "PWM", "DAC", "UART", "MQTT"];
|
|
4
|
+
|
|
5
|
+
/** Interface for a channel instance in the workflow */
|
|
6
|
+
export interface Channel {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
type: ChannelType;
|
|
10
|
+
arguments: Record<string, unknown>;
|
|
11
|
+
}
|
|
@@ -1,76 +1,76 @@
|
|
|
1
|
-
import type { Parameter } from "../parameter";
|
|
2
|
-
import { ALL_CHANNEL_TYPES } from "./Channel";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* Single union definition for all Channel variants — deliberately NOT the
|
|
6
|
-
* per-type registry pattern used by Node/Memory/Model (`*Definition` +
|
|
7
|
-
* `*Registry`, one definition object per type).
|
|
8
|
-
*
|
|
9
|
-
* The reason is in-place `type` switching that preserves shared parameter state.
|
|
10
|
-
* Because `type` is a parameter and all variants share one `arguments` bag,
|
|
11
|
-
* changing a channel's type keeps the same instance (id, label) and retains any
|
|
12
|
-
* entered values that are still valid for the new type: the parameter +
|
|
13
|
-
* `activationRules` machinery just re-gates which fields show, and `serialize`
|
|
14
|
-
* (via `pruneArguments`) drops the now-inactive ones. A per-type registry
|
|
15
|
-
* would make each variant its own definition, so switching type would mean
|
|
16
|
-
* delete-and-recreate — losing id/label and every shared value the user entered.
|
|
17
|
-
* (It also lets channels add from one "Add Channel" button with type as a field,
|
|
18
|
-
* rather than an add-button per type. Node/Memory/Model instead treat `type` as
|
|
19
|
-
* an immutable registry key chosen at creation.)
|
|
20
|
-
*
|
|
21
|
-
* The same could be achieved with a UI-layer "type selector" over a
|
|
22
|
-
* ChannelRegistry; this union trades cross-family consistency for that
|
|
23
|
-
* shared-state-preserving switch.
|
|
24
|
-
*/
|
|
25
|
-
export interface ChannelDefinition {
|
|
26
|
-
parameters: Parameter[];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export const CHANNEL_DEFINITION: ChannelDefinition = {
|
|
30
|
-
parameters: [
|
|
31
|
-
{
|
|
32
|
-
id: "type",
|
|
33
|
-
label: "Type",
|
|
34
|
-
description: "Channel type",
|
|
35
|
-
type: "selection",
|
|
36
|
-
default: "GPIOIN",
|
|
37
|
-
options: ALL_CHANNEL_TYPES.map((t) => ({ value: t, label: t })),
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
id: "bias",
|
|
41
|
-
label: "Bias",
|
|
42
|
-
description: "Pin bias configuration",
|
|
43
|
-
type: "selection",
|
|
44
|
-
default: "none",
|
|
45
|
-
options: [
|
|
46
|
-
{ value: "none", label: "None" },
|
|
47
|
-
{ value: "pullup", label: "Pull-up" },
|
|
48
|
-
{ value: "pulldown", label: "Pull-down" },
|
|
49
|
-
],
|
|
50
|
-
activationRules: [{ type: "parameterIn", parameterId: "type", values: ["GPIOIN"] }],
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
id: "debounceMs",
|
|
54
|
-
label: "Debounce (ms)",
|
|
55
|
-
description: "Debounce window in milliseconds",
|
|
56
|
-
type: "int",
|
|
57
|
-
default: 50,
|
|
58
|
-
activationRules: [{ type: "parameterIn", parameterId: "type", values: ["GPIOIN"] }],
|
|
59
|
-
},
|
|
60
|
-
{
|
|
61
|
-
id: "frequency",
|
|
62
|
-
label: "Frequency (Hz)",
|
|
63
|
-
description: "PWM frequency in Hz",
|
|
64
|
-
type: "int",
|
|
65
|
-
default: 1000,
|
|
66
|
-
activationRules: [{ type: "parameterIn", parameterId: "type", values: ["PWM"] }],
|
|
67
|
-
},
|
|
68
|
-
{
|
|
69
|
-
id: "topic",
|
|
70
|
-
label: "Topic",
|
|
71
|
-
description: "MQTT topic this channel publishes to / subscribes on (e.g. sensors/temperature)",
|
|
72
|
-
type: "string",
|
|
73
|
-
activationRules: [{ type: "parameterIn", parameterId: "type", values: ["MQTT"] }],
|
|
74
|
-
},
|
|
75
|
-
],
|
|
76
|
-
};
|
|
1
|
+
import type { Parameter } from "../parameter";
|
|
2
|
+
import { ALL_CHANNEL_TYPES } from "./Channel";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Single union definition for all Channel variants — deliberately NOT the
|
|
6
|
+
* per-type registry pattern used by Node/Memory/Model (`*Definition` +
|
|
7
|
+
* `*Registry`, one definition object per type).
|
|
8
|
+
*
|
|
9
|
+
* The reason is in-place `type` switching that preserves shared parameter state.
|
|
10
|
+
* Because `type` is a parameter and all variants share one `arguments` bag,
|
|
11
|
+
* changing a channel's type keeps the same instance (id, label) and retains any
|
|
12
|
+
* entered values that are still valid for the new type: the parameter +
|
|
13
|
+
* `activationRules` machinery just re-gates which fields show, and `serialize`
|
|
14
|
+
* (via `pruneArguments`) drops the now-inactive ones. A per-type registry
|
|
15
|
+
* would make each variant its own definition, so switching type would mean
|
|
16
|
+
* delete-and-recreate — losing id/label and every shared value the user entered.
|
|
17
|
+
* (It also lets channels add from one "Add Channel" button with type as a field,
|
|
18
|
+
* rather than an add-button per type. Node/Memory/Model instead treat `type` as
|
|
19
|
+
* an immutable registry key chosen at creation.)
|
|
20
|
+
*
|
|
21
|
+
* The same could be achieved with a UI-layer "type selector" over a
|
|
22
|
+
* ChannelRegistry; this union trades cross-family consistency for that
|
|
23
|
+
* shared-state-preserving switch.
|
|
24
|
+
*/
|
|
25
|
+
export interface ChannelDefinition {
|
|
26
|
+
parameters: Parameter[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const CHANNEL_DEFINITION: ChannelDefinition = {
|
|
30
|
+
parameters: [
|
|
31
|
+
{
|
|
32
|
+
id: "type",
|
|
33
|
+
label: "Type",
|
|
34
|
+
description: "Channel type",
|
|
35
|
+
type: "selection",
|
|
36
|
+
default: "GPIOIN",
|
|
37
|
+
options: ALL_CHANNEL_TYPES.map((t) => ({ value: t, label: t })),
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: "bias",
|
|
41
|
+
label: "Bias",
|
|
42
|
+
description: "Pin bias configuration",
|
|
43
|
+
type: "selection",
|
|
44
|
+
default: "none",
|
|
45
|
+
options: [
|
|
46
|
+
{ value: "none", label: "None" },
|
|
47
|
+
{ value: "pullup", label: "Pull-up" },
|
|
48
|
+
{ value: "pulldown", label: "Pull-down" },
|
|
49
|
+
],
|
|
50
|
+
activationRules: [{ type: "parameterIn", parameterId: "type", values: ["GPIOIN"] }],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
id: "debounceMs",
|
|
54
|
+
label: "Debounce (ms)",
|
|
55
|
+
description: "Debounce window in milliseconds",
|
|
56
|
+
type: "int",
|
|
57
|
+
default: 50,
|
|
58
|
+
activationRules: [{ type: "parameterIn", parameterId: "type", values: ["GPIOIN"] }],
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
id: "frequency",
|
|
62
|
+
label: "Frequency (Hz)",
|
|
63
|
+
description: "PWM frequency in Hz",
|
|
64
|
+
type: "int",
|
|
65
|
+
default: 1000,
|
|
66
|
+
activationRules: [{ type: "parameterIn", parameterId: "type", values: ["PWM"] }],
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: "topic",
|
|
70
|
+
label: "Topic",
|
|
71
|
+
description: "MQTT topic this channel publishes to / subscribes on (e.g. sensors/temperature)",
|
|
72
|
+
type: "string",
|
|
73
|
+
activationRules: [{ type: "parameterIn", parameterId: "type", values: ["MQTT"] }],
|
|
74
|
+
},
|
|
75
|
+
],
|
|
76
|
+
};
|
package/src/channel/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
export type { ChannelType, Channel } from "./Channel";
|
|
2
|
-
export { ALL_CHANNEL_TYPES } from "./Channel";
|
|
3
|
-
export type { ChannelDefinition } from "./ChannelDefinition";
|
|
4
|
-
export { CHANNEL_DEFINITION } from "./ChannelDefinition";
|
|
5
|
-
export { serialize, deserialize } from "./serialization";
|
|
6
|
-
export type { ApiChannel } from "./serialization";
|
|
1
|
+
export type { ChannelType, Channel } from "./Channel";
|
|
2
|
+
export { ALL_CHANNEL_TYPES } from "./Channel";
|
|
3
|
+
export type { ChannelDefinition } from "./ChannelDefinition";
|
|
4
|
+
export { CHANNEL_DEFINITION } from "./ChannelDefinition";
|
|
5
|
+
export { serialize, deserialize } from "./serialization";
|
|
6
|
+
export type { ApiChannel } from "./serialization";
|
|
@@ -1,68 +1,68 @@
|
|
|
1
|
-
import type { Schemas } from "../api";
|
|
2
|
-
import { pruneArguments } from "../parameter";
|
|
3
|
-
import { CHANNEL_DEFINITION } from "./ChannelDefinition";
|
|
4
|
-
import type { Channel } from "./Channel";
|
|
5
|
-
|
|
6
|
-
export type ApiChannel = Schemas["Channel"];
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Serialize a domain Channel to the API discriminated-union shape.
|
|
10
|
-
*
|
|
11
|
-
* The domain store retains inactive parameters (non-destructive type switching),
|
|
12
|
-
* so this is the boundary that drops them. The `type` discriminator must be in
|
|
13
|
-
* the args record so `parameterIn` rules can evaluate — otherwise every gated
|
|
14
|
-
* field (bias/debounceMs/frequency) is stripped as "inactive". Physical
|
|
15
|
-
* addressing (GPIO line, ADC/PWM/DAC channel) is not here — it's a deploy
|
|
16
|
-
* binding, not workflow state.
|
|
17
|
-
*/
|
|
18
|
-
export function serialize(ch: Channel): ApiChannel {
|
|
19
|
-
const { id, label, type } = ch;
|
|
20
|
-
const args: Record<string, unknown> = { ...ch.arguments, type };
|
|
21
|
-
pruneArguments(args, CHANNEL_DEFINITION.parameters);
|
|
22
|
-
switch (type) {
|
|
23
|
-
case "GPIOIN":
|
|
24
|
-
return {
|
|
25
|
-
type,
|
|
26
|
-
id,
|
|
27
|
-
label,
|
|
28
|
-
bias: args.bias as Schemas["GPIOINChannel"]["bias"],
|
|
29
|
-
debounceMs: args.debounceMs as number,
|
|
30
|
-
};
|
|
31
|
-
case "GPIOOUT":
|
|
32
|
-
return { type, id, label };
|
|
33
|
-
case "ADC":
|
|
34
|
-
return { type, id, label };
|
|
35
|
-
case "PWM":
|
|
36
|
-
return { type, id, label, frequency: args.frequency as number };
|
|
37
|
-
case "DAC":
|
|
38
|
-
return { type, id, label };
|
|
39
|
-
case "UART":
|
|
40
|
-
return { type, id, label };
|
|
41
|
-
case "MQTT":
|
|
42
|
-
return { type, id, label, topic: args.topic as string };
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Convert an API Channel into a domain Channel. */
|
|
47
|
-
export function deserialize(api: ApiChannel): Channel {
|
|
48
|
-
const { id, label, type } = api;
|
|
49
|
-
const args: Record<string, unknown> = {};
|
|
50
|
-
switch (type) {
|
|
51
|
-
case "GPIOIN":
|
|
52
|
-
args.bias = api.bias;
|
|
53
|
-
args.debounceMs = api.debounceMs;
|
|
54
|
-
break;
|
|
55
|
-
case "PWM":
|
|
56
|
-
args.frequency = api.frequency;
|
|
57
|
-
break;
|
|
58
|
-
case "MQTT":
|
|
59
|
-
args.topic = api.topic;
|
|
60
|
-
break;
|
|
61
|
-
case "GPIOOUT":
|
|
62
|
-
case "ADC":
|
|
63
|
-
case "DAC":
|
|
64
|
-
case "UART":
|
|
65
|
-
break;
|
|
66
|
-
}
|
|
67
|
-
return { id, label, type, arguments: args };
|
|
68
|
-
}
|
|
1
|
+
import type { Schemas } from "../api";
|
|
2
|
+
import { pruneArguments } from "../parameter";
|
|
3
|
+
import { CHANNEL_DEFINITION } from "./ChannelDefinition";
|
|
4
|
+
import type { Channel } from "./Channel";
|
|
5
|
+
|
|
6
|
+
export type ApiChannel = Schemas["Channel"];
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Serialize a domain Channel to the API discriminated-union shape.
|
|
10
|
+
*
|
|
11
|
+
* The domain store retains inactive parameters (non-destructive type switching),
|
|
12
|
+
* so this is the boundary that drops them. The `type` discriminator must be in
|
|
13
|
+
* the args record so `parameterIn` rules can evaluate — otherwise every gated
|
|
14
|
+
* field (bias/debounceMs/frequency) is stripped as "inactive". Physical
|
|
15
|
+
* addressing (GPIO line, ADC/PWM/DAC channel) is not here — it's a deploy
|
|
16
|
+
* binding, not workflow state.
|
|
17
|
+
*/
|
|
18
|
+
export function serialize(ch: Channel): ApiChannel {
|
|
19
|
+
const { id, label, type } = ch;
|
|
20
|
+
const args: Record<string, unknown> = { ...ch.arguments, type };
|
|
21
|
+
pruneArguments(args, CHANNEL_DEFINITION.parameters);
|
|
22
|
+
switch (type) {
|
|
23
|
+
case "GPIOIN":
|
|
24
|
+
return {
|
|
25
|
+
type,
|
|
26
|
+
id,
|
|
27
|
+
label,
|
|
28
|
+
bias: args.bias as Schemas["GPIOINChannel"]["bias"],
|
|
29
|
+
debounceMs: args.debounceMs as number,
|
|
30
|
+
};
|
|
31
|
+
case "GPIOOUT":
|
|
32
|
+
return { type, id, label };
|
|
33
|
+
case "ADC":
|
|
34
|
+
return { type, id, label };
|
|
35
|
+
case "PWM":
|
|
36
|
+
return { type, id, label, frequency: args.frequency as number };
|
|
37
|
+
case "DAC":
|
|
38
|
+
return { type, id, label };
|
|
39
|
+
case "UART":
|
|
40
|
+
return { type, id, label };
|
|
41
|
+
case "MQTT":
|
|
42
|
+
return { type, id, label, topic: args.topic as string };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Convert an API Channel into a domain Channel. */
|
|
47
|
+
export function deserialize(api: ApiChannel): Channel {
|
|
48
|
+
const { id, label, type } = api;
|
|
49
|
+
const args: Record<string, unknown> = {};
|
|
50
|
+
switch (type) {
|
|
51
|
+
case "GPIOIN":
|
|
52
|
+
args.bias = api.bias;
|
|
53
|
+
args.debounceMs = api.debounceMs;
|
|
54
|
+
break;
|
|
55
|
+
case "PWM":
|
|
56
|
+
args.frequency = api.frequency;
|
|
57
|
+
break;
|
|
58
|
+
case "MQTT":
|
|
59
|
+
args.topic = api.topic;
|
|
60
|
+
break;
|
|
61
|
+
case "GPIOOUT":
|
|
62
|
+
case "ADC":
|
|
63
|
+
case "DAC":
|
|
64
|
+
case "UART":
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
return { id, label, type, arguments: args };
|
|
68
|
+
}
|
package/src/deploy/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export { getReferencedCatalogModelIds } from "./requirements";
|
|
1
|
+
export { getReferencedCatalogModelIds } from "./requirements";
|
|
@@ -1,61 +1,61 @@
|
|
|
1
|
-
import { describe, it, expect } from "vitest";
|
|
2
|
-
import { getReferencedCatalogModelIds } from "./requirements";
|
|
3
|
-
import { MAIN_CANVAS_ID, type Workflow, type Canvas } from "../workflow";
|
|
4
|
-
import type { Node } from "../node";
|
|
5
|
-
import type { Model } from "../model";
|
|
6
|
-
|
|
7
|
-
// Minimal Agent node referencing `model`. Cast through the union — only id/type/
|
|
8
|
-
// arguments.model matter to the walk.
|
|
9
|
-
function agent(id: string, model: string): Node {
|
|
10
|
-
return {
|
|
11
|
-
id,
|
|
12
|
-
type: "Agent",
|
|
13
|
-
position: { x: 0, y: 0 },
|
|
14
|
-
arguments: {
|
|
15
|
-
name: id,
|
|
16
|
-
model,
|
|
17
|
-
instructions: "",
|
|
18
|
-
outputDeclarations: [],
|
|
19
|
-
memoryRefs: [],
|
|
20
|
-
answer: { active: true, mode: "emit", name: "answer" },
|
|
21
|
-
},
|
|
22
|
-
} as Node;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function canvas(nodes: Node[]): Canvas {
|
|
26
|
-
return { nodes, edges: [], variables: {} };
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const customModel: Model = { id: "custom-llm", label: "Custom", type: "LLMModel", arguments: {} };
|
|
30
|
-
|
|
31
|
-
function workflow(canvases: Workflow["canvases"], models: Record<string, Model> = {}): Workflow {
|
|
32
|
-
return { canvases, functions: {}, channels: {}, memory: {}, models };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
describe("getReferencedCatalogModelIds", () => {
|
|
36
|
-
it("returns catalog ids (referenced but not declared), excluding declared customs", () => {
|
|
37
|
-
const wf = workflow(
|
|
38
|
-
{ [MAIN_CANVAS_ID]: canvas([agent("n1", "claude-opus-4-7"), agent("n2", "custom-llm")]) },
|
|
39
|
-
{ "custom-llm": customModel },
|
|
40
|
-
);
|
|
41
|
-
expect(getReferencedCatalogModelIds(wf)).toEqual(["claude-opus-4-7"]);
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it("ignores unset model references", () => {
|
|
45
|
-
const wf = workflow({ [MAIN_CANVAS_ID]: canvas([agent("n1", "")]) });
|
|
46
|
-
expect(getReferencedCatalogModelIds(wf)).toEqual([]);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it("dedupes across nodes and walks every canvas (main + function bodies)", () => {
|
|
50
|
-
const wf = workflow({
|
|
51
|
-
[MAIN_CANVAS_ID]: canvas([agent("n1", "claude-opus-4-7"), agent("n2", "claude-opus-4-7")]),
|
|
52
|
-
fnBody: canvas([agent("n3", "gemini-2")]),
|
|
53
|
-
});
|
|
54
|
-
expect(getReferencedCatalogModelIds(wf).sort()).toEqual(["claude-opus-4-7", "gemini-2"]);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("returns nothing when every referenced model is declared", () => {
|
|
58
|
-
const wf = workflow({ [MAIN_CANVAS_ID]: canvas([agent("n1", "custom-llm")]) }, { "custom-llm": customModel });
|
|
59
|
-
expect(getReferencedCatalogModelIds(wf)).toEqual([]);
|
|
60
|
-
});
|
|
61
|
-
});
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { getReferencedCatalogModelIds } from "./requirements";
|
|
3
|
+
import { MAIN_CANVAS_ID, type Workflow, type Canvas } from "../workflow";
|
|
4
|
+
import type { Node } from "../node";
|
|
5
|
+
import type { Model } from "../model";
|
|
6
|
+
|
|
7
|
+
// Minimal Agent node referencing `model`. Cast through the union — only id/type/
|
|
8
|
+
// arguments.model matter to the walk.
|
|
9
|
+
function agent(id: string, model: string): Node {
|
|
10
|
+
return {
|
|
11
|
+
id,
|
|
12
|
+
type: "Agent",
|
|
13
|
+
position: { x: 0, y: 0 },
|
|
14
|
+
arguments: {
|
|
15
|
+
name: id,
|
|
16
|
+
model,
|
|
17
|
+
instructions: "",
|
|
18
|
+
outputDeclarations: [],
|
|
19
|
+
memoryRefs: [],
|
|
20
|
+
answer: { active: true, mode: "emit", name: "answer" },
|
|
21
|
+
},
|
|
22
|
+
} as Node;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function canvas(nodes: Node[]): Canvas {
|
|
26
|
+
return { nodes, edges: [], variables: {} };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const customModel: Model = { id: "custom-llm", label: "Custom", type: "LLMModel", arguments: {} };
|
|
30
|
+
|
|
31
|
+
function workflow(canvases: Workflow["canvases"], models: Record<string, Model> = {}): Workflow {
|
|
32
|
+
return { canvases, functions: {}, channels: {}, memory: {}, models };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("getReferencedCatalogModelIds", () => {
|
|
36
|
+
it("returns catalog ids (referenced but not declared), excluding declared customs", () => {
|
|
37
|
+
const wf = workflow(
|
|
38
|
+
{ [MAIN_CANVAS_ID]: canvas([agent("n1", "claude-opus-4-7"), agent("n2", "custom-llm")]) },
|
|
39
|
+
{ "custom-llm": customModel },
|
|
40
|
+
);
|
|
41
|
+
expect(getReferencedCatalogModelIds(wf)).toEqual(["claude-opus-4-7"]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("ignores unset model references", () => {
|
|
45
|
+
const wf = workflow({ [MAIN_CANVAS_ID]: canvas([agent("n1", "")]) });
|
|
46
|
+
expect(getReferencedCatalogModelIds(wf)).toEqual([]);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("dedupes across nodes and walks every canvas (main + function bodies)", () => {
|
|
50
|
+
const wf = workflow({
|
|
51
|
+
[MAIN_CANVAS_ID]: canvas([agent("n1", "claude-opus-4-7"), agent("n2", "claude-opus-4-7")]),
|
|
52
|
+
fnBody: canvas([agent("n3", "gemini-2")]),
|
|
53
|
+
});
|
|
54
|
+
expect(getReferencedCatalogModelIds(wf).sort()).toEqual(["claude-opus-4-7", "gemini-2"]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("returns nothing when every referenced model is declared", () => {
|
|
58
|
+
const wf = workflow({ [MAIN_CANVAS_ID]: canvas([agent("n1", "custom-llm")]) }, { "custom-llm": customModel });
|
|
59
|
+
expect(getReferencedCatalogModelIds(wf)).toEqual([]);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
import type { Workflow } from "../workflow";
|
|
2
|
-
import { NodeRegistry, isNodeUsedAsTool } from "../node";
|
|
3
|
-
import { isParameterActive } from "../parameter";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Model ids that nodes reference but the workflow does not declare in `models`.
|
|
7
|
-
* A `modelSelect` accepts exactly two sources — declared custom models and the
|
|
8
|
-
* static catalog — so any referenced id that isn't a declared model is a catalog
|
|
9
|
-
* model: it carries no declared config, yet still needs a provider/credential
|
|
10
|
-
* supplied at deploy.
|
|
11
|
-
*
|
|
12
|
-
* This is the one deploy demand the workflow's resource arrays can't express:
|
|
13
|
-
* channels/memory/declared-models are enumerable directly from
|
|
14
|
-
* `workflow.{channels,memory,models}`, but catalog model ids live only on the
|
|
15
|
-
* nodes that pick them — hence the walk. Spans every canvas (main + function
|
|
16
|
-
* bodies) and honors parameter activation, so a model behind an inactive
|
|
17
|
-
* `modelSelect` (pruned on serialize, never deployed) is not counted.
|
|
18
|
-
*/
|
|
19
|
-
export function getReferencedCatalogModelIds(workflow: Workflow): string[] {
|
|
20
|
-
const declaredModel = new Set(Object.keys(workflow.models));
|
|
21
|
-
const catalogIds = new Set<string>();
|
|
22
|
-
|
|
23
|
-
for (const canvas of Object.values(workflow.canvases)) {
|
|
24
|
-
for (const node of canvas.nodes) {
|
|
25
|
-
const def = NodeRegistry.getByType(node.type);
|
|
26
|
-
if (!def) continue;
|
|
27
|
-
const args = node.arguments as Record<string, unknown>;
|
|
28
|
-
const isToolInput = isNodeUsedAsTool(node.id, node, canvas.edges);
|
|
29
|
-
for (const param of def.parameters) {
|
|
30
|
-
if (param.type !== "modelSelect") continue;
|
|
31
|
-
if (!isParameterActive(param, args, isToolInput)) continue;
|
|
32
|
-
const id = args[param.id];
|
|
33
|
-
if (typeof id === "string" && id !== "" && !declaredModel.has(id)) {
|
|
34
|
-
catalogIds.add(id);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
return [...catalogIds];
|
|
41
|
-
}
|
|
1
|
+
import type { Workflow } from "../workflow";
|
|
2
|
+
import { NodeRegistry, isNodeUsedAsTool } from "../node";
|
|
3
|
+
import { isParameterActive } from "../parameter";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Model ids that nodes reference but the workflow does not declare in `models`.
|
|
7
|
+
* A `modelSelect` accepts exactly two sources — declared custom models and the
|
|
8
|
+
* static catalog — so any referenced id that isn't a declared model is a catalog
|
|
9
|
+
* model: it carries no declared config, yet still needs a provider/credential
|
|
10
|
+
* supplied at deploy.
|
|
11
|
+
*
|
|
12
|
+
* This is the one deploy demand the workflow's resource arrays can't express:
|
|
13
|
+
* channels/memory/declared-models are enumerable directly from
|
|
14
|
+
* `workflow.{channels,memory,models}`, but catalog model ids live only on the
|
|
15
|
+
* nodes that pick them — hence the walk. Spans every canvas (main + function
|
|
16
|
+
* bodies) and honors parameter activation, so a model behind an inactive
|
|
17
|
+
* `modelSelect` (pruned on serialize, never deployed) is not counted.
|
|
18
|
+
*/
|
|
19
|
+
export function getReferencedCatalogModelIds(workflow: Workflow): string[] {
|
|
20
|
+
const declaredModel = new Set(Object.keys(workflow.models));
|
|
21
|
+
const catalogIds = new Set<string>();
|
|
22
|
+
|
|
23
|
+
for (const canvas of Object.values(workflow.canvases)) {
|
|
24
|
+
for (const node of canvas.nodes) {
|
|
25
|
+
const def = NodeRegistry.getByType(node.type);
|
|
26
|
+
if (!def) continue;
|
|
27
|
+
const args = node.arguments as Record<string, unknown>;
|
|
28
|
+
const isToolInput = isNodeUsedAsTool(node.id, node, canvas.edges);
|
|
29
|
+
for (const param of def.parameters) {
|
|
30
|
+
if (param.type !== "modelSelect") continue;
|
|
31
|
+
if (!isParameterActive(param, args, isToolInput)) continue;
|
|
32
|
+
const id = args[param.id];
|
|
33
|
+
if (typeof id === "string" && id !== "" && !declaredModel.has(id)) {
|
|
34
|
+
catalogIds.add(id);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return [...catalogIds];
|
|
41
|
+
}
|