@formio/uag-sdk 0.0.0-stage → 2.0.0-rc.2

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/define.js ADDED
@@ -0,0 +1,106 @@
1
+ 'use strict';
2
+
3
+ /** The engine contract version this SDK describes. */
4
+ const ENGINE_API_VERSION = 1;
5
+
6
+ /** Engine type and instance names: what `UAG_ENGINES` and routing lists use. */
7
+ const ENGINE_NAME = /^[a-z][a-z0-9-]{0,62}$/;
8
+
9
+ const KINDS = ['decision', 'agent'];
10
+ const SETTING_NAME = /^[a-z][a-zA-Z0-9]*$/;
11
+ const VARIABLE_NAME = /^[A-Z][A-Z0-9_]*$/;
12
+ const SETTING_TYPES = ['string', 'number', 'boolean'];
13
+ const SETTING_KEYS = ['env', 'required', 'secret', 'default', 'type', 'description'];
14
+
15
+ /** The reasons a value is not a valid engine type definition; empty when it is. */
16
+ function definitionProblems(definition) {
17
+ if (!definition || typeof definition !== 'object') {
18
+ return ['it is not an object'];
19
+ }
20
+ const problems = [];
21
+ if (typeof definition.type !== 'string' || !ENGINE_NAME.test(definition.type)) {
22
+ problems.push('"type" must be a name of lowercase letters, digits and "-", starting with a letter');
23
+ }
24
+ if (!Array.isArray(definition.kinds) || !definition.kinds.length || !definition.kinds.every((kind) => KINDS.includes(kind))) {
25
+ problems.push('"kinds" must list "decision", "agent" or both');
26
+ }
27
+ if (!Number.isInteger(definition.apiVersion) || definition.apiVersion < 1) {
28
+ problems.push('"apiVersion" must be the engine contract version it was built for');
29
+ }
30
+ if (typeof definition.create !== 'function') {
31
+ problems.push('"create" must be a function');
32
+ }
33
+ const settings = definition.settings === undefined ? {} : definition.settings;
34
+ if (!settings || typeof settings !== 'object' || Array.isArray(settings)) {
35
+ problems.push('"settings" must be an object');
36
+ }
37
+ else {
38
+ for (const [key, spec] of Object.entries(settings)) {
39
+ problems.push(...settingProblems(key, spec));
40
+ }
41
+ }
42
+ return problems;
43
+ }
44
+
45
+ function settingProblems(key, spec) {
46
+ const label = `setting "${key}"`;
47
+ if (!SETTING_NAME.test(key)) {
48
+ return [`${label} must be a camelCase name`];
49
+ }
50
+ if (!spec || typeof spec !== 'object') {
51
+ return [`${label} must be an object`];
52
+ }
53
+ const problems = [];
54
+ const unknown = Object.keys(spec).filter((name) => !SETTING_KEYS.includes(name));
55
+ if (unknown.length) {
56
+ problems.push(`${label} has unknown keys: ${unknown.join(', ')}`);
57
+ }
58
+ const env = spec.env === undefined ? [] : [].concat(spec.env);
59
+ if (!env.every((name) => typeof name === 'string' && VARIABLE_NAME.test(name))) {
60
+ problems.push(`${label}: "env" must name upper-case environment variables`);
61
+ }
62
+ if (spec.type !== undefined && !SETTING_TYPES.includes(spec.type)) {
63
+ problems.push(`${label}: "type" must be one of ${SETTING_TYPES.join(', ')}`);
64
+ }
65
+ for (const flag of ['required', 'secret']) {
66
+ if (spec[flag] !== undefined && typeof spec[flag] !== 'boolean') {
67
+ problems.push(`${label}: "${flag}" must be true or false`);
68
+ }
69
+ }
70
+ if (spec.default !== undefined && typeof spec.default !== (spec.type || 'string')) {
71
+ problems.push(`${label}: "default" must be a ${spec.type || 'string'}`);
72
+ }
73
+ return problems;
74
+ }
75
+
76
+ /** Whether a value is a valid engine type definition, from this SDK or any other copy of it. */
77
+ function isEngineType(value) {
78
+ return definitionProblems(value).length === 0;
79
+ }
80
+
81
+ /**
82
+ * Define an engine type. Validates the definition and returns it frozen, so a
83
+ * mistake in it fails when the package loads rather than when the UAG starts:
84
+ *
85
+ * module.exports = defineEngine({
86
+ * type: 'acme',
87
+ * kinds: ['agent'],
88
+ * apiVersion: ENGINE_API_VERSION,
89
+ * settings: { apiKey: { env: 'ACME_API_KEY', required: true, secret: true } },
90
+ * create({ name, settings }) { return { name, kind: 'agent', model: '...', extract }; },
91
+ * });
92
+ */
93
+ function defineEngine(definition) {
94
+ const problems = definitionProblems(definition);
95
+ if (problems.length) {
96
+ const type = definition && typeof definition.type === 'string' ? ` "${definition.type}"` : '';
97
+ throw new TypeError(`The engine type${type} is not valid: ${problems.join('; ')}.`);
98
+ }
99
+ const settings = {};
100
+ for (const [key, spec] of Object.entries(definition.settings || {})) {
101
+ settings[key] = Object.freeze({ ...spec, ...(spec.env !== undefined && { env: Object.freeze([].concat(spec.env)) }) });
102
+ }
103
+ return Object.freeze({ ...definition, kinds: Object.freeze([...definition.kinds]), settings: Object.freeze(settings) });
104
+ }
105
+
106
+ module.exports = { ENGINE_API_VERSION, ENGINE_NAME, KINDS, defineEngine, definitionProblems, isEngineType };
package/errors.js ADDED
@@ -0,0 +1,43 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * An engine failure whose message is safe to show to API callers, such as
5
+ * "Quota exceeded for this API key". The UAG reports any other engine error only
6
+ * as "the engine failed (see the logs for request X)", because a provider SDK's
7
+ * error can carry URLs or account details.
8
+ */
9
+ class EngineError extends Error {
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = 'EngineError';
13
+ Object.defineProperty(this, 'expose', { value: true, enumerable: true });
14
+ }
15
+ }
16
+
17
+ /**
18
+ * Thrown when an engine instance cannot be enabled. The message names the
19
+ * instance, and is what the operator reads when the UAG refuses to start.
20
+ */
21
+ class EngineConfigError extends Error {
22
+ constructor(message) {
23
+ super(message);
24
+ this.name = 'EngineConfigError';
25
+ }
26
+ }
27
+
28
+ /** Shorter values are too likely to occur by chance to be worth redacting. */
29
+ const MIN_SECRET_LENGTH = 4;
30
+
31
+ /** `text` with every occurrence of each secret value replaced. */
32
+ function redact(text, secrets) {
33
+ let result = String(text);
34
+ for (const secret of secrets || []) {
35
+ const value = secret === undefined || secret === null ? '' : String(secret);
36
+ if (value.length >= MIN_SECRET_LENGTH) {
37
+ result = result.split(value).join('[redacted]');
38
+ }
39
+ }
40
+ return result;
41
+ }
42
+
43
+ module.exports = { EngineError, EngineConfigError, redact };
package/helpers.js ADDED
@@ -0,0 +1,145 @@
1
+ 'use strict';
2
+ const { EngineError } = require('./errors');
3
+
4
+ /**
5
+ * Helpers shared by agent engines built on model APIs with structured outputs.
6
+ */
7
+
8
+ /**
9
+ * The extraction schema with every field required, for strict structured
10
+ * outputs: providers limit how many optional properties and union types a
11
+ * strict schema may have, and OpenAI's strict mode needs every property in
12
+ * `required`. An empty value stands for "not stated": "" for text and options,
13
+ * [] for lists. Numbers and booleans travel as strings, keeping the field's
14
+ * title and description. `decode` maps an answer
15
+ * back to the UAG's schema, leaving the "not stated" fields out.
16
+ *
17
+ * const { schema, decode } = requiredSchema(request.schema);
18
+ * // ... ask the model for `schema` ...
19
+ * return { model, values: decode(answer) };
20
+ */
21
+ function requiredSchema(schema) {
22
+ const { schema: sent, decoders } = encodeObject(schema, false);
23
+ return { schema: sent, decode: (values) => decodeValues(values, decoders) };
24
+ }
25
+
26
+ function encodeObject(schema, nested) {
27
+ const properties = {};
28
+ const decoders = {};
29
+ for (const [path, property] of Object.entries((schema && schema.properties) || {})) {
30
+ const { sent, decode } = encodeProperty(property, nested);
31
+ properties[path] = sent;
32
+ if (decode) decoders[path] = decode;
33
+ }
34
+ return { schema: { ...schema, properties, required: Object.keys(properties) }, decoders };
35
+ }
36
+
37
+ function encodeProperty(property, nested) {
38
+ // Before the number and boolean cases, so a typed enum keeps its values.
39
+ if (Array.isArray(property.enum)) {
40
+ if (property.enum.every((value) => typeof value === 'string')) {
41
+ return { sent: { ...property, enum: [...new Set([...property.enum, ''])] } };
42
+ }
43
+ // Other values travel as their JSON text. A provider that does not
44
+ // enforce the schema may answer anything: only an allowed value is kept.
45
+ const { type, ...rest } = property;
46
+ const allowed = new Map(property.enum.map((value) => [JSON.stringify(value), value]));
47
+ return {
48
+ sent: { ...rest, type: 'string', enum: [...allowed.keys(), ''] },
49
+ decode: (value) => (allowed.has(value) ? allowed.get(value) : undefined),
50
+ };
51
+ }
52
+ if (property.type === 'number' || property.type === 'integer') {
53
+ return {
54
+ sent: { type: 'string', ...described(property, NUMBER_HINT) },
55
+ decode: parseNumber,
56
+ };
57
+ }
58
+ // Inside an object (select boxes) false already means "not selected".
59
+ if (property.type === 'boolean' && !nested) {
60
+ return {
61
+ sent: { enum: ['true', 'false', ''], ...described(property) },
62
+ decode: (value) => (value === 'true' || value === true ? true : value === 'false' || value === false ? false : undefined),
63
+ };
64
+ }
65
+ if (property.type === 'object' && property.properties) {
66
+ const inner = encodeObject(property, true);
67
+ return {
68
+ sent: inner.schema,
69
+ decode: (value) => {
70
+ const decoded = decodeValues(value, inner.decoders);
71
+ return Object.keys(decoded).length ? decoded : undefined;
72
+ },
73
+ };
74
+ }
75
+ return { sent: property };
76
+ }
77
+
78
+ const NUMBER_HINT = 'Answer in digits only, such as 4250 or -3.5, with no currency or units.';
79
+
80
+ /** The field's title and description, which a string encoding would otherwise drop, with a hint. */
81
+ function described(property, hint) {
82
+ const description = [property.description, hint].filter(Boolean).join(' ');
83
+ return { ...(property.title && { title: property.title }), ...(description && { description }) };
84
+ }
85
+
86
+ /**
87
+ * A number from a model's answer. Models write amounts as they read them
88
+ * ("$4,250"), so a currency sign and thousands separators are accepted;
89
+ * anything else that is not a plain number is "not stated".
90
+ */
91
+ function parseNumber(value) {
92
+ if (typeof value === 'number') return Number.isFinite(value) ? value : undefined;
93
+ if (typeof value !== 'string') return undefined;
94
+ const match = /^\s*([+-])?\s*[$€£¥]?\s*(\d[\d,]*(?:\.\d+)?|\.\d+)\s*[$€£¥]?\s*$/.exec(value);
95
+ if (!match) return undefined;
96
+ const digits = match[2];
97
+ if (digits.includes(',') && !/^\d{1,3}(,\d{3})+(\.\d+)?$/.test(digits)) return undefined;
98
+ const number = Number(`${match[1] || ''}${digits.replace(/,/g, '')}`);
99
+ return Number.isFinite(number) ? number : undefined;
100
+ }
101
+
102
+ /** The answer without the empty "not stated" values, in the UAG's types. */
103
+ function decodeValues(values, decoders) {
104
+ const decoded = {};
105
+ if (!values || typeof values !== 'object' || Array.isArray(values)) return decoded;
106
+ for (const [path, value] of Object.entries(values)) {
107
+ if (value === '' || value === null || (Array.isArray(value) && !value.length)) continue;
108
+ const result = decoders[path] ? decoders[path](value) : value;
109
+ if (result !== undefined) decoded[path] = result;
110
+ }
111
+ return decoded;
112
+ }
113
+
114
+ /**
115
+ * The first JSON object in a model's text answer, tolerating code fences and
116
+ * prose around it. Throws an `EngineError` when there is none.
117
+ */
118
+ function parseJsonObject(text) {
119
+ const unfenced = String(text || '').replace(/```(?:json)?/gi, '');
120
+ const start = unfenced.indexOf('{');
121
+ const end = unfenced.lastIndexOf('}');
122
+ let parsed;
123
+ if (start !== -1 && end > start) {
124
+ try {
125
+ parsed = JSON.parse(unfenced.slice(start, end + 1));
126
+ }
127
+ catch {
128
+ parsed = undefined;
129
+ }
130
+ }
131
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
132
+ throw new EngineError('The engine did not return a JSON object.');
133
+ }
134
+ return parsed;
135
+ }
136
+
137
+ /** A tool call's result as text for the model: its text blocks, other blocks as JSON. */
138
+ function toolResultText(result) {
139
+ const text = ((result && result.content) || [])
140
+ .map((block) => (block && typeof block.text === 'string' ? block.text : JSON.stringify(block)))
141
+ .join('\n');
142
+ return text || '(no output)';
143
+ }
144
+
145
+ module.exports = { requiredSchema, decodeValues, parseJsonObject, toolResultText };
package/index.d.ts ADDED
@@ -0,0 +1,102 @@
1
+ import type { Engine } from './contract';
2
+
3
+ export * from './contract';
4
+
5
+ /** Engine type and instance names: lowercase letters, digits and "-", starting with a letter. */
6
+ export declare const ENGINE_NAME: RegExp;
7
+
8
+ /** One setting an engine type reads. */
9
+ export type SettingSpec = {
10
+ /** The type's own environment variables for the setting, read after `UAG_ENGINE_<NAME>_<SETTING>`. */
11
+ env?: string | readonly string[];
12
+ /** Startup fails, naming the instance and the variables, when no value is set. */
13
+ required?: boolean;
14
+ /** Kept out of startup errors and capability discovery. */
15
+ secret?: boolean;
16
+ default?: string | number | boolean;
17
+ /** Values from the environment are converted to this type. Default: "string". */
18
+ type?: 'string' | 'number' | 'boolean';
19
+ description?: string;
20
+ };
21
+
22
+ /** What an engine type's `create` receives for one instance. */
23
+ export type EngineCreateContext<Settings = Record<string, any>> = {
24
+ /** The instance name. The engine must use it as its `name`. */
25
+ name: string;
26
+ /** The instance's value for each declared setting that has one. */
27
+ settings: Settings;
28
+ /** The instance's options as given in configuration, including ones that are not settings (such as a test client). */
29
+ options: Record<string, any>;
30
+ /** The host's configuration, read-only (for example routing timeouts). */
31
+ config: Record<string, any>;
32
+ /** A setting's value, or the standard "needs its setting" error, for settings required only in some cases. */
33
+ requireSetting(key: string): any;
34
+ };
35
+
36
+ /** What an engine package exports. */
37
+ export interface EngineTypeDefinition<Settings = Record<string, any>> {
38
+ /** The type name, as in `UAG_ENGINES=<type>` and `<name>:<type>`. */
39
+ type: string;
40
+ kinds: readonly ('decision' | 'agent')[];
41
+ /** The engine contract version the type was built for: `ENGINE_API_VERSION`. */
42
+ apiVersion: number;
43
+ description?: string;
44
+ settings?: Record<string, SettingSpec>;
45
+ create(context: EngineCreateContext<Settings>): Engine | Promise<Engine>;
46
+ }
47
+
48
+ /** Validate an engine type definition and return it frozen. Throws a TypeError naming every problem. */
49
+ export declare function defineEngine<T extends EngineTypeDefinition<any>>(definition: T): Readonly<T>;
50
+
51
+ /** Whether a value is a valid engine type definition, from this SDK, another copy of it, or none. */
52
+ export declare function isEngineType(value: unknown): value is EngineTypeDefinition;
53
+
54
+ /**
55
+ * An engine failure whose message is safe to show to API callers. Other engine
56
+ * errors are reported only as "the engine failed (see the logs for request X)".
57
+ */
58
+ export declare class EngineError extends Error {
59
+ readonly expose: true;
60
+ constructor(message: string);
61
+ }
62
+
63
+ /** An engine instance that cannot be enabled. The message names the instance. */
64
+ export declare class EngineConfigError extends Error {
65
+ constructor(message: string);
66
+ }
67
+
68
+ /** `text` with each secret value replaced by "[redacted]". */
69
+ export declare function redact(text: unknown, secrets: unknown[]): string;
70
+
71
+ /** An engine instance to enable. `name` is undefined until loading when only a package or definition was given. */
72
+ export type EngineInstanceSpec = {
73
+ name?: string;
74
+ type: string | EngineTypeDefinition;
75
+ options?: Record<string, any>;
76
+ };
77
+
78
+ /** Parse a `UAG_ENGINES` string (`jev,fast:claude`) or a configuration list. Nothing named means none. */
79
+ export declare function parseInstances(source: string | (string | EngineInstanceSpec)[] | undefined | null): EngineInstanceSpec[];
80
+
81
+ /** The instances from configuration (`engines`) when it lists them, otherwise from `UAG_ENGINES`. */
82
+ export declare function engineInstances(config?: Record<string, any>, env?: Record<string, string | undefined>): EngineInstanceSpec[];
83
+
84
+ /** An instance's own variable for a setting: `UAG_ENGINE_<NAME>_<SETTING>`. */
85
+ export declare function settingVariable(instance: string, setting: string): string;
86
+
87
+ /** An instance's settings: options, then `UAG_ENGINE_<NAME>_<SETTING>`, then the type's variables, then the default. */
88
+ export declare function resolveSettings(definition: EngineTypeDefinition, name: string, options?: Record<string, any>, env?: Record<string, string | undefined>): Record<string, any>;
89
+
90
+ /**
91
+ * The extraction schema with every field required, for strict structured
92
+ * outputs, and `decode`, which maps an answer back to the UAG's schema.
93
+ */
94
+ export declare function requiredSchema(schema: Record<string, any>): { schema: Record<string, any>; decode(values: unknown): Record<string, any> };
95
+
96
+ export declare function decodeValues(values: unknown, decoders: Record<string, (value: any) => any>): Record<string, any>;
97
+
98
+ /** The first JSON object in a model's text answer. Throws an `EngineError` when there is none. */
99
+ export declare function parseJsonObject(text: string): Record<string, any>;
100
+
101
+ /** A tool call's result as text for a model. */
102
+ export declare function toolResultText(result: { content?: { type: string; text?: string; [key: string]: any }[] }): string;
package/index.js ADDED
@@ -0,0 +1,30 @@
1
+ 'use strict';
2
+ /**
3
+ * @formio/uag-sdk: what engine packages for the Form.io UAG are built
4
+ * with. The engine contract's types are in contract.d.ts; the loader is
5
+ * `@formio/uag-sdk/loader` and the conformance check
6
+ * `@formio/uag-sdk/conformance`.
7
+ */
8
+ const { ENGINE_API_VERSION, ENGINE_NAME, defineEngine, isEngineType } = require('./define');
9
+ const { EngineError, EngineConfigError, redact } = require('./errors');
10
+ const { parseInstances, engineInstances } = require('./instances');
11
+ const { settingVariable, resolveSettings } = require('./settings');
12
+ const { requiredSchema, decodeValues, parseJsonObject, toolResultText } = require('./helpers');
13
+
14
+ module.exports = {
15
+ ENGINE_API_VERSION,
16
+ ENGINE_NAME,
17
+ defineEngine,
18
+ isEngineType,
19
+ EngineError,
20
+ EngineConfigError,
21
+ redact,
22
+ parseInstances,
23
+ engineInstances,
24
+ settingVariable,
25
+ resolveSettings,
26
+ requiredSchema,
27
+ decodeValues,
28
+ parseJsonObject,
29
+ toolResultText,
30
+ };
package/instances.js ADDED
@@ -0,0 +1,117 @@
1
+ 'use strict';
2
+ const { ENGINE_NAME } = require('./define');
3
+ const { EngineConfigError } = require('./errors');
4
+
5
+ /**
6
+ * npm package names, scoped or not. Every segment starts with a letter or digit,
7
+ * so "." and ".." (and so any relative path) can never match.
8
+ */
9
+ const PACKAGE_NAME = /^(?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*$/;
10
+ const MAX_PACKAGE_NAME = 214;
11
+
12
+ /**
13
+ * Whether a type is given as a package name rather than a type name: scoped
14
+ * (`@acme/uag-engine-x`), or unscoped with the conventional prefix (`uag-engine-x`).
15
+ */
16
+ function isPackageName(type) {
17
+ return type.startsWith('@') || type.includes('/') || type.startsWith('uag-engine-');
18
+ }
19
+
20
+ /** Refuse a type that is neither a type name nor a package name, before any lookup. */
21
+ function checkType(type) {
22
+ const valid = isPackageName(type)
23
+ ? PACKAGE_NAME.test(type) && type.length <= MAX_PACKAGE_NAME
24
+ : ENGINE_NAME.test(type);
25
+ if (!valid) {
26
+ throw new EngineConfigError(
27
+ `The engine type name ${JSON.stringify(type)} is not valid. Use a type name such as "claude", ` +
28
+ 'or a package name such as "@acme/uag-engine-x" or "uag-engine-x".'
29
+ );
30
+ }
31
+ }
32
+
33
+ function checkName(name) {
34
+ if (!ENGINE_NAME.test(name)) {
35
+ throw new EngineConfigError(
36
+ `The engine name ${JSON.stringify(name)} is not valid: use lowercase letters, digits and "-", ` +
37
+ 'starting with a letter, up to 63 characters.'
38
+ );
39
+ }
40
+ }
41
+
42
+ /** Refuse two instances with the same name. Instances without a name yet are skipped. */
43
+ function checkUnique(instances) {
44
+ const seen = new Set();
45
+ for (const { name } of instances) {
46
+ if (!name) continue;
47
+ if (seen.has(name)) {
48
+ throw new EngineConfigError(`Two engines are named "${name}". Engine instance names must be unique.`);
49
+ }
50
+ seen.add(name);
51
+ }
52
+ }
53
+
54
+ /** One `UAG_ENGINES` entry: `<type>` or `<name>:<type>`. */
55
+ function parseEntry(entry) {
56
+ const colon = entry.indexOf(':');
57
+ const name = colon === -1 ? undefined : entry.slice(0, colon).trim();
58
+ const type = colon === -1 ? entry : entry.slice(colon + 1).trim();
59
+ if (name !== undefined) checkName(name);
60
+ checkType(type);
61
+ // A bare package name takes its type's name, which is known once it loads.
62
+ return { name: name ?? (isPackageName(type) ? undefined : type), type, options: {} };
63
+ }
64
+
65
+ /** One configuration entry: a `UAG_ENGINES` string, or `{ name, type, options }`. */
66
+ function parseConfigured(entry) {
67
+ if (typeof entry === 'string') {
68
+ return parseEntry(entry.trim());
69
+ }
70
+ if (!entry || typeof entry !== 'object') {
71
+ throw new EngineConfigError(`An engine entry must be a string or { name, type, options }, not ${JSON.stringify(entry)}.`);
72
+ }
73
+ const { name, type, options = {} } = entry;
74
+ if (name !== undefined) checkName(name);
75
+ if (typeof type === 'string') {
76
+ checkType(type);
77
+ }
78
+ else if (!type || typeof type !== 'object') {
79
+ throw new EngineConfigError(`The engine${name ? ` "${name}"` : ''} needs a type: a type name, a package name, or an engine type definition.`);
80
+ }
81
+ if (!options || typeof options !== 'object' || Array.isArray(options)) {
82
+ throw new EngineConfigError(`The options of the engine${name ? ` "${name}"` : ''} must be an object.`);
83
+ }
84
+ const named = name ?? (typeof type === 'string' ? (isPackageName(type) ? undefined : type) : undefined);
85
+ return { name: named, type, options };
86
+ }
87
+
88
+ /**
89
+ * The engine instances in a `UAG_ENGINES` string or a configuration list. Each
90
+ * is `{ name, type, options }`; `name` is undefined until the type is known when
91
+ * the entry names only a package or a definition. Nothing named means none.
92
+ */
93
+ function parseInstances(source) {
94
+ if (source === undefined || source === null) return [];
95
+ let entries;
96
+ if (typeof source === 'string') {
97
+ entries = source.split(',').map((entry) => entry.trim()).filter(Boolean).map(parseEntry);
98
+ }
99
+ else if (Array.isArray(source)) {
100
+ entries = source.filter((entry) => entry !== '').map(parseConfigured);
101
+ }
102
+ else {
103
+ throw new EngineConfigError('The engine list must be a comma-separated string or an array.');
104
+ }
105
+ checkUnique(entries);
106
+ return entries;
107
+ }
108
+
109
+ /**
110
+ * The instances to enable: from configuration (`engines`) when it lists them,
111
+ * otherwise from `UAG_ENGINES`. Configuration takes precedence.
112
+ */
113
+ function engineInstances(config = {}, env = process.env) {
114
+ return parseInstances(Array.isArray(config && config.engines) ? config.engines : env.UAG_ENGINES);
115
+ }
116
+
117
+ module.exports = { PACKAGE_NAME, isPackageName, checkName, checkType, checkUnique, parseInstances, engineInstances };
package/loader.d.ts ADDED
@@ -0,0 +1,48 @@
1
+ import type { Engine } from './contract';
2
+ import type { EngineInstanceSpec, EngineTypeDefinition } from './index';
3
+
4
+ export type LoadOptions = {
5
+ /** Types resolved before any package: the built-in ones, and ones the embedding application passes. */
6
+ types?: Record<string, EngineTypeDefinition>;
7
+ /** The directories whose `node_modules` packages are looked up in, from `enginePaths`; nowhere else. */
8
+ paths?: string[];
9
+ env?: Record<string, string | undefined>;
10
+ config?: Record<string, any>;
11
+ /** The engine contract version the host supports. Default: this SDK's. */
12
+ apiVersion?: number;
13
+ };
14
+
15
+ /** An enabled engine and where it came from, ready for `Formflow.engineRegistry`. */
16
+ export type LoadedEngine = {
17
+ engine: Engine;
18
+ type: string;
19
+ package?: string;
20
+ version?: string;
21
+ };
22
+
23
+ /**
24
+ * The directories whose `node_modules` packages are looked up in: the host's own
25
+ * installation and each directory above it, then each absolute directory in
26
+ * `UAG_ENGINE_PATH` (without the directories above it).
27
+ */
28
+ export declare function enginePaths(root: string | undefined, env?: Record<string, string | undefined>): string[];
29
+
30
+ /** The packages a type name may come from, in order. */
31
+ export declare function packageCandidates(type: string): string[];
32
+
33
+ export declare function resolveType(type: string | EngineTypeDefinition, options?: LoadOptions): {
34
+ definition: EngineTypeDefinition;
35
+ package?: string;
36
+ version?: string;
37
+ };
38
+
39
+ /** Create one engine for an instance, resolving its settings. */
40
+ export declare function createEngine(definition: EngineTypeDefinition, options: {
41
+ name: string;
42
+ options?: Record<string, any>;
43
+ env?: Record<string, string | undefined>;
44
+ config?: Record<string, any>;
45
+ }): Promise<Engine>;
46
+
47
+ /** Resolve, configure and create each instance. Any problem stops loading, with the instance named. */
48
+ export declare function loadEngines(instances: EngineInstanceSpec[], options?: LoadOptions): Promise<LoadedEngine[]>;