@uipath/maestro-builder-sdk 6.60.0 → 6.61.1
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/README.md +10 -12
- package/dist/api-index.md +17 -16
- package/dist/api-members.md +48 -48
- package/dist/check-cli.js +8 -7
- package/dist/check.d.ts +11 -0
- package/dist/check.js +192 -783
- package/dist/cli-library.d.ts +2 -1
- package/dist/cli-library.js +7 -2
- package/dist/cli-result.d.ts +14 -0
- package/dist/cli-result.js +18 -1
- package/dist/compile-cli.js +2 -2
- package/dist/core/compile-refusal.d.ts +7 -1
- package/dist/core/compile-refusal.js +9 -1
- package/dist/core/connector-checks.d.ts +8 -30
- package/dist/core/connector-checks.js +11 -60
- package/dist/core/connector-inputs.js +9 -1
- package/dist/core/event-checks.d.ts +15 -2
- package/dist/core/event-checks.js +28 -13
- package/dist/core/hitl-routing.d.ts +10 -1
- package/dist/core/hitl-routing.js +20 -5
- package/dist/core/lookups.d.ts +13 -0
- package/dist/core/lookups.js +26 -0
- package/dist/core/refusals.d.ts +3 -0
- package/dist/core/refusals.js +6 -1
- package/dist/flow-node-type.d.ts +35 -21
- package/dist/flow-parallel-hitl.d.ts +105 -0
- package/dist/flow-parallel-hitl.js +214 -0
- package/dist/flow-sdk.d.ts +4 -4
- package/dist/flow-sdk.js +2 -2
- package/dist/flow-terminate-names.d.ts +2 -2
- package/dist/flow-terminate-names.js +2 -2
- package/dist/merge-compile.js +2 -1
- package/dist/nodes/agent/flow/check.js +13 -88
- package/dist/nodes/agent/flow/inline-agent.js +41 -14
- package/dist/nodes/connector/flow/node-type.js +45 -18
- package/dist/nodes/document/flow/ixp.d.ts +1 -1
- package/dist/nodes/document/flow/ixp.js +22 -26
- package/dist/nodes/hitl/flow/check.js +22 -13
- package/dist/nodes/hitl/flow/node-type.js +16 -7
- package/dist/nodes/hitl/flow/spec.d.ts +11 -5
- package/dist/nodes/http/flow/check.js +5 -31
- package/dist/nodes/http/flow/node-type.js +7 -4
- package/dist/nodes/queue/flow/check.d.ts +4 -3
- package/dist/nodes/queue/flow/check.js +7 -6
- package/dist/nodes/queue/flow/node-type.js +1 -1
- package/dist/nodes/transform/flow/check.d.ts +2 -0
- package/dist/nodes/transform/flow/check.js +3 -6
- package/dist/nodes/transform/flow/node-type.js +6 -6
- package/dist/raw-node.js +8 -8
- package/dist/serialize.d.ts +38 -3
- package/dist/serialize.js +757 -322
- package/package.json +2 -2
package/dist/serialize.js
CHANGED
|
@@ -12,11 +12,13 @@ import { Expr, toExpr, SCHEDULE_PRESETS, flatNameSegmentsOf, parallelFork } from
|
|
|
12
12
|
import { FlowAction, FlowTrigger } from './core/node-classes.js';
|
|
13
13
|
import { connectorRawNodeRefusal } from './core/connector-raw-node.js';
|
|
14
14
|
import { CompileRefusal } from './core/compile-refusal.js';
|
|
15
|
+
import { nearestName } from './core/edit-distance.js';
|
|
15
16
|
import { rawNodeInputProblem, registeredRawType } from './raw-node-advice.js';
|
|
16
17
|
import { planContributionBindings } from './core/contribution-bindings.js';
|
|
17
18
|
import { declaredErrorPortRefusal, definitionHasErrorPort, errorPortRefusal } from './core/error-port.js';
|
|
18
19
|
import { FLOW_NODE_TYPES } from './flow-node-registry.js';
|
|
19
20
|
import { stepExits } from './flow-step-exits.js';
|
|
21
|
+
import { endOutcomeRefusal, fanInRefusal, hitlEndHazards, hitlFanInto } from './flow-parallel-hitl.js';
|
|
20
22
|
import { resolveVersionCarried } from './core/nodes/versioned.js';
|
|
21
23
|
import { indexCarriedDefinitions } from './core/carried-definitions.js';
|
|
22
24
|
import { connectionConnectors } from './bindings-v2.js';
|
|
@@ -30,7 +32,7 @@ import { hasExits } from './core/step-ports.js';
|
|
|
30
32
|
import { stopsAtEnd } from './flow-step-tree.js';
|
|
31
33
|
import { assertNodeEnvelope } from './core/node-envelope.js';
|
|
32
34
|
import { fnv1aX4Uuid, stableId } from './core/stable-id.js';
|
|
33
|
-
import { readEventFilter, eventFilterProblem, eventFilterJmes, eventFilterTreeLeaf, jmesString, jmesStringProblem } from './core/event-filters.js';
|
|
35
|
+
import { EVENT_FILTER_OPERATORS, readEventFilter, eventFilterProblem, eventFilterJmes, eventFilterTreeLeaf, jmesString, jmesStringProblem } from './core/event-filters.js';
|
|
34
36
|
import { FLOW_EXPR_DIALECT } from './core/connector-checks.js';
|
|
35
37
|
import { connectorInputErrors as coreConnectorInputErrors } from './core/connector-inputs.js';
|
|
36
38
|
/**
|
|
@@ -532,8 +534,7 @@ function refuseUnverifiedByoa(opts, where, sub) {
|
|
|
532
534
|
* INLINE_AGENT_TOOL_CONNECTOR_NO_CONNECTION, and serialize leaves it unbound.)
|
|
533
535
|
* `resolveBinding` returns `undefined` for an absent label, which used to reach
|
|
534
536
|
* the node as `detail.connectionId: null` with no binding — valid to compile,
|
|
535
|
-
* refused by `flow validate` (flow-builder-sdk#890).
|
|
536
|
-
* sentence first (BINDING_MISSING).
|
|
537
|
+
* refused by `flow validate` (flow-builder-sdk#890): BINDING_MISSING.
|
|
537
538
|
*/
|
|
538
539
|
function requireBindingPair(where, labels, target) {
|
|
539
540
|
const missing = missingBindingLabels(labels);
|
|
@@ -709,6 +710,71 @@ export function resolveContributionDefinition(ref, requestedVersion, where) {
|
|
|
709
710
|
}
|
|
710
711
|
throw new CompileRefusal('CONTRIBUTION_INVALID', `${where}: unknown DefinitionReference source "${ref.source}".`);
|
|
711
712
|
}
|
|
713
|
+
/**
|
|
714
|
+
* `library.resolveEvent` for one subscription, or EVENT_UNKNOWN: whatever it
|
|
715
|
+
* throws (a connector or event the library does not carry, a version or object
|
|
716
|
+
* it does not resolve) is that one condition, an event compile cannot place.
|
|
717
|
+
* `where` names the site as `check` always has (`trigger`, `waitForEvent "x"`).
|
|
718
|
+
*/
|
|
719
|
+
function resolveEvent(library, kind, sub, where) {
|
|
720
|
+
try {
|
|
721
|
+
return library.resolveEvent(kind, sub.connector, sub.event, sub.version, sub.object);
|
|
722
|
+
}
|
|
723
|
+
catch (error) {
|
|
724
|
+
if (error instanceof Error && error.message.startsWith('internal SDK error:'))
|
|
725
|
+
throw error;
|
|
726
|
+
throw new CompileRefusal('EVENT_UNKNOWN', `${where}: ${error instanceof Error ? error.message : String(error)}`);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* LIBRARY_REQUIRED for an event subscription at `site`, after the refusals that
|
|
731
|
+
* need no library: its binding labels, judged as `eventDetail` judges them, so a
|
|
732
|
+
* run without a library (`check`'s inner loop) still hears about them.
|
|
733
|
+
*/
|
|
734
|
+
function libraryRequiredForEvent(opts, sub, nodeId, site, call) {
|
|
735
|
+
if (sub !== null && typeof sub === 'object') {
|
|
736
|
+
if (Array.isArray(sub.filters))
|
|
737
|
+
eventFilterReads(sub.filters, nodeId);
|
|
738
|
+
requireBindingPair(site, sub, eventTarget(sub));
|
|
739
|
+
resolveBinding(opts, sub.connection, 'connection', `event node "${nodeId}"`, eventTarget(sub));
|
|
740
|
+
resolveBinding(opts, sub.folder, 'folder', `event node "${nodeId}"`, eventTarget(sub));
|
|
741
|
+
}
|
|
742
|
+
return new CompileRefusal('LIBRARY_REQUIRED', `${call} needs a library (pass --library / set $FLOW_SDK_LIBRARY_JSON).`);
|
|
743
|
+
}
|
|
744
|
+
/** The trigger kinds a `.trigger()` or `.entryPoint()` spec may carry. */
|
|
745
|
+
const TRIGGER_KINDS = new Set(['manual', 'scheduled', 'event', 'form', 'conversation', 'voice']);
|
|
746
|
+
/**
|
|
747
|
+
* Refuse a trigger spec that is not one: types are stripped at run time, so
|
|
748
|
+
* `.trigger({ every: 'R/PT1H' })` (the spec without its factory) or a typo'd
|
|
749
|
+
* kind arrives intact. `call` is how the author wrote the site (`.trigger()`,
|
|
750
|
+
* `.entryPoint("ep")`).
|
|
751
|
+
*/
|
|
752
|
+
function refuseUnknownTrigger(spec, call) {
|
|
753
|
+
const kind = spec?.kind;
|
|
754
|
+
if (typeof kind === 'string' && TRIGGER_KINDS.has(kind))
|
|
755
|
+
return;
|
|
756
|
+
throw new CompileRefusal('TRIGGER_UNKNOWN_KIND', `${call} was given ${JSON.stringify(kind)}, which is not a trigger. `
|
|
757
|
+
+ `Wrap it in a factory: scheduled({ every: 'R/PT1H' }) for a schedule, `
|
|
758
|
+
+ `onEvent({ connector, event, where, connection, folder }) to start on a connector event, formTrigger() to start from a `
|
|
759
|
+
+ `submitted form, or conversationTrigger() to start on a new conversation. Omit .trigger() entirely `
|
|
760
|
+
+ `for the manual trigger (the default).`, `scheduled({ every: 'R/PT1H' })`);
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* A custom node's contribution. A `contribute()` that throws something other
|
|
764
|
+
* than a refusal (an author's own `Error`) is the node's refusal,
|
|
765
|
+
* CONTRIBUTION_INVALID, so `check` reports it on the node and `compile` names
|
|
766
|
+
* the code. An `internal SDK error:` is passed on as it is.
|
|
767
|
+
*/
|
|
768
|
+
function contributionOf(node, requestedVersion, where) {
|
|
769
|
+
try {
|
|
770
|
+
return node.contribute(requestedVersion !== undefined ? { requestedVersion } : {});
|
|
771
|
+
}
|
|
772
|
+
catch (error) {
|
|
773
|
+
if (error instanceof CompileRefusal || (error instanceof Error && error.message.startsWith('internal SDK error:')))
|
|
774
|
+
throw error;
|
|
775
|
+
throw new CompileRefusal('CONTRIBUTION_INVALID', `compile cannot lower ${where}: ${error instanceof Error ? error.message : String(error)}`);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
712
778
|
/**
|
|
713
779
|
* Whether `def` declares `port` as a forward source handle: an exact id, or a
|
|
714
780
|
* templated one (`outcome-{item.id}` declares every `outcome-<slug>`). `error` is
|
|
@@ -806,6 +872,43 @@ function flattenWhere(where, declared) {
|
|
|
806
872
|
walk(where, '');
|
|
807
873
|
return out;
|
|
808
874
|
}
|
|
875
|
+
/**
|
|
876
|
+
* A subscription's `filters`, each leaf read structurally (`readEventFilter`),
|
|
877
|
+
* or the refusal of the first leaf compile cannot lower: no field, no operator
|
|
878
|
+
* or two, a value no operator takes, a backslash JMESPath engines read apart.
|
|
879
|
+
* Needs no library, so a run without one judges the leaves too
|
|
880
|
+
* (`libraryRequiredForEvent`).
|
|
881
|
+
*/
|
|
882
|
+
function eventFilterReads(filters, nodeId) {
|
|
883
|
+
const operators = Object.keys(EVENT_FILTER_OPERATORS).join(', ');
|
|
884
|
+
const at = `event node "${nodeId}": filters`;
|
|
885
|
+
return filters.map((f, i) => {
|
|
886
|
+
const { keys, read } = readEventFilter(f);
|
|
887
|
+
const field = f?.field;
|
|
888
|
+
if (typeof field !== 'string' || field === '') {
|
|
889
|
+
throw new CompileRefusal('EVENT_FILTER_NO_FIELD', `${at}[${i}] has no \`field\` — name a field on the event payload, e.g. 'subject'.`);
|
|
890
|
+
}
|
|
891
|
+
if (keys.length === 0) {
|
|
892
|
+
throw new CompileRefusal('EVENT_FILTER_NO_VALUE', `${at}[${i}] has no operator. Write exactly one of ${operators} with its value — `
|
|
893
|
+
+ `\`contains\` is a case-sensitive substring test, the same one Integration Service applies `
|
|
894
|
+
+ `server-side; \`lessThan\`/\`greaterThan\` order a number or an ISO date.`, `{ field: '${field}', contains: '<text>' }`);
|
|
895
|
+
}
|
|
896
|
+
if (keys.length > 1) {
|
|
897
|
+
throw new CompileRefusal('EVENT_FILTER_MANY_OPERATORS', `${at}[${i}] carries ${keys.length} operators (${keys.join(', ')}); a condition has exactly `
|
|
898
|
+
+ `one. Write two conditions instead — they are ANDed.`);
|
|
899
|
+
}
|
|
900
|
+
if (!read) {
|
|
901
|
+
throw new CompileRefusal('EVENT_FILTER_NO_VALUE', `${at}[${i}] gives \`${keys[0]}\` a value that is not a string, number or boolean.`);
|
|
902
|
+
}
|
|
903
|
+
// An ambiguous backslash first: `eventFilterProblem` would report it too.
|
|
904
|
+
const problem = eventFilterProblem(read);
|
|
905
|
+
const backslash = typeof read.value === 'string' && jmesStringProblem(read.value) !== undefined;
|
|
906
|
+
if (problem) {
|
|
907
|
+
throw new CompileRefusal(backslash ? 'EVENT_FILTER_BACKSLASH' : 'EVENT_FILTER_BAD_VALUE', `${at}[${i}] on \`${read.field}\`: ${problem}`);
|
|
908
|
+
}
|
|
909
|
+
return read;
|
|
910
|
+
});
|
|
911
|
+
}
|
|
809
912
|
function eventDetail(rc, sub, nodeId, opts, site) {
|
|
810
913
|
requireBindingPair(site, sub, eventTarget(sub));
|
|
811
914
|
const conn = resolveBinding(opts, sub.connection, 'connection', `event node "${nodeId}"`, eventTarget(sub));
|
|
@@ -817,8 +920,13 @@ function eventDetail(rc, sub, nodeId, opts, site) {
|
|
|
817
920
|
// would otherwise emit it, with the text every family's compile shares.
|
|
818
921
|
const objectName = String(op.objectName ?? '');
|
|
819
922
|
const noObject = genericEventObjectRefusal(rc, sub, (o) => `onEvent(…, { object: '${o}' })`);
|
|
820
|
-
if (noObject)
|
|
821
|
-
|
|
923
|
+
if (noObject) {
|
|
924
|
+
// The object an author put in `where` by mistake is the value to move.
|
|
925
|
+
const scopeObject = sub.where && typeof sub.where === 'object'
|
|
926
|
+
? Object.entries(sub.where).find(([k]) => /^(objectName|entityName|object)$/i.test(k))?.[1]
|
|
927
|
+
: undefined;
|
|
928
|
+
throw new CompileRefusal('EVENT_GENERIC_NO_OBJECT', `event node "${nodeId}": ${noObject}`, `object: '${scopeObject !== undefined ? String(scopeObject) : '<Object>'}'`);
|
|
929
|
+
}
|
|
822
930
|
// An unresolved `where` lookup is swapped for its recorded id — the id
|
|
823
931
|
// `prepare --resolve` stored under the EVENT-form node type — or refused.
|
|
824
932
|
// Same rule as connector inputs: emitting a placeholder for a value nobody
|
|
@@ -871,27 +979,8 @@ function eventDetail(rc, sub, nodeId, opts, site) {
|
|
|
871
979
|
});
|
|
872
980
|
// Each authored filter is read structurally (which operator, which value) and
|
|
873
981
|
// lowered with the designer's own grammar — see `event-filters.ts` for the
|
|
874
|
-
// measured table.
|
|
875
|
-
|
|
876
|
-
const conditions = filters.map((f, i) => {
|
|
877
|
-
const { keys, read } = readEventFilter(f);
|
|
878
|
-
if (!read) {
|
|
879
|
-
// The code `check` gives the same leaf (`core/event-checks.ts`).
|
|
880
|
-
const field = f?.field;
|
|
881
|
-
throw new CompileRefusal(typeof field !== 'string' || field === '' ? 'EVENT_FILTER_NO_FIELD'
|
|
882
|
-
: keys.length > 1 ? 'EVENT_FILTER_MANY_OPERATORS' : 'EVENT_FILTER_NO_VALUE', `event node "${nodeId}": filters[${i}] must carry a field and exactly ONE operator `
|
|
883
|
-
+ `(contains, startsWith, endsWith, equals, notEquals, lessThan, lessThanOrEqual, greaterThan, `
|
|
884
|
-
+ `greaterThanOrEqual) — got ${JSON.stringify(f)}.`);
|
|
885
|
-
}
|
|
886
|
-
// An ambiguous backslash first, under the code `check` gives it
|
|
887
|
-
// (`core/event-checks.ts`); `eventFilterProblem` would report it too.
|
|
888
|
-
const problem = eventFilterProblem(read);
|
|
889
|
-
const backslash = typeof read.value === 'string' && jmesStringProblem(read.value) !== undefined;
|
|
890
|
-
if (problem) {
|
|
891
|
-
throw new CompileRefusal(backslash ? 'EVENT_FILTER_BACKSLASH' : 'EVENT_FILTER_BAD_VALUE', `event node "${nodeId}": filters[${i}] on \`${read.field}\`: ${problem}`);
|
|
892
|
-
}
|
|
893
|
-
return read;
|
|
894
|
-
});
|
|
982
|
+
// measured table.
|
|
983
|
+
const conditions = eventFilterReads(filters, nodeId);
|
|
895
984
|
const filterClauses = conditions.map((c) => `(${eventFilterJmes(c)})`);
|
|
896
985
|
const filterExpression = [
|
|
897
986
|
...scopeClauses,
|
|
@@ -1032,10 +1121,10 @@ function misplacedLookupMessage(token) {
|
|
|
1032
1121
|
/**
|
|
1033
1122
|
* The flow's declarations as the globals it will emit: a same-named `.output()`
|
|
1034
1123
|
* and `.var()` folded into one `out` global (flow-builder-sdk#899), every other
|
|
1035
|
-
* repeated name refused
|
|
1036
|
-
*
|
|
1037
|
-
*
|
|
1038
|
-
*
|
|
1124
|
+
* repeated name refused (VARIABLE_NAME_COLLISION / OUTPUT_VAR_MISMATCH), so no
|
|
1125
|
+
* path (`compile`, `emitFlow`, a direct `serialize()`) can write a file
|
|
1126
|
+
* `flow validate` rejects for a duplicate variable id; `check` reports the
|
|
1127
|
+
* refusal from its serializer run.
|
|
1039
1128
|
*/
|
|
1040
1129
|
function emittableGlobals(built, where, kind = 'root') {
|
|
1041
1130
|
// Entry-point inputs are rows of the same globals[], so they share the
|
|
@@ -1046,21 +1135,21 @@ function emittableGlobals(built, where, kind = 'root') {
|
|
|
1046
1135
|
const shared = sharedInputsOf(built, where, kind);
|
|
1047
1136
|
const plan = planFlowGlobals({ inputs, outputs: built.outputs, vars: [...built.vars, ...shared.autoVars] });
|
|
1048
1137
|
if (plan.problems.length) {
|
|
1049
|
-
throw new CompileRefusal(plan.problems[0].code, `${where}: ${plan.problems.map((p) => `[${p.code}] ${p.message}`).join(' ')}
|
|
1138
|
+
throw new CompileRefusal(plan.problems[0].code, `${where}: ${plan.problems.map((p) => `[${p.code}] ${p.message}`).join(' ')}`, plan.problems[0].suggestion);
|
|
1050
1139
|
}
|
|
1051
1140
|
// An output the flow sets from inside gets an `inout` Variable to hold the
|
|
1052
1141
|
// value; the End copies it into the Output (#1092, `flow-output-backing.ts`).
|
|
1053
1142
|
const sharedOutputs = new Set(shared.targets.filter((t) => t.declaredAs === 'output').map((t) => t.name));
|
|
1054
1143
|
const backing = planOutputBacking(built, plan.outputs, plan.vars, sharedOutputs);
|
|
1055
1144
|
if (backing.problems.length) {
|
|
1056
|
-
throw new CompileRefusal(backing.problems[0].code, `${where}: ${backing.problems.map((p) => `[${p.code}] ${p.message}`).join(' ')}
|
|
1145
|
+
throw new CompileRefusal(backing.problems[0].code, `${where}: ${backing.problems.map((p) => `[${p.code}] ${p.message}`).join(' ')}`, backing.problems[0].suggestion);
|
|
1057
1146
|
}
|
|
1058
1147
|
return { ...plan, backing: backing.backing, backingReads: backing.reads };
|
|
1059
1148
|
}
|
|
1060
1149
|
/**
|
|
1061
|
-
* The flow's `{ shared }` input plan (#924), refused here
|
|
1062
|
-
*
|
|
1063
|
-
*
|
|
1150
|
+
* The flow's `{ shared }` input plan (#924), its errors refused here (and
|
|
1151
|
+
* reported by `check` from its serializer run). Warnings (a root that feeds
|
|
1152
|
+
* nothing) are `check`'s alone.
|
|
1064
1153
|
*/
|
|
1065
1154
|
function sharedInputsOf(built, where, kind) {
|
|
1066
1155
|
if (kind === 'subflow') {
|
|
@@ -1072,8 +1161,9 @@ function sharedInputsOf(built, where, kind) {
|
|
|
1072
1161
|
}
|
|
1073
1162
|
const plan = planSharedInputs({ roots: sharedInputRoots(built), outputs: built.outputs, vars: built.vars });
|
|
1074
1163
|
const errors = plan.problems.filter((p) => p.level === 'error');
|
|
1075
|
-
if (errors.length)
|
|
1076
|
-
throw new CompileRefusal(errors[0].code, `${where}: ${errors.map((p) => `[${p.code}] ${p.message}`).join(' ')}
|
|
1164
|
+
if (errors.length) {
|
|
1165
|
+
throw new CompileRefusal(errors[0].code, `${where}: ${errors.map((p) => `[${p.code}] ${p.message}`).join(' ')}`, errors[0].suggestion);
|
|
1166
|
+
}
|
|
1077
1167
|
return plan;
|
|
1078
1168
|
}
|
|
1079
1169
|
function newScope(outputs, vars = [], backing = new Map(), backingReads = backing) {
|
|
@@ -1147,14 +1237,22 @@ function newScope(outputs, vars = [], backing = new Map(), backingReads = backin
|
|
|
1147
1237
|
* exact class of failure `.onError` used to have (an error path that vanishes in
|
|
1148
1238
|
* conversion with every rung green). So it throws instead.
|
|
1149
1239
|
*/
|
|
1150
|
-
function resolveRejoins(scope, addEdge
|
|
1240
|
+
function resolveRejoins(scope, addEdge,
|
|
1241
|
+
/** Refuse an edge whose target is unknown: throws, or (collect mode) reports it on the step it leaves and returns. */
|
|
1242
|
+
refuse = (_from, refusal) => { throw refusal; }) {
|
|
1151
1243
|
for (const { from, target } of scope.pendingEdges) {
|
|
1152
1244
|
const id = scope.stepNodeId.get(target) ?? scope.endNodeId.get(target);
|
|
1153
1245
|
if (!id) {
|
|
1246
|
+
const steps = [...scope.stepNodeId.keys()];
|
|
1154
1247
|
const ends = [...scope.endNodeId.keys()];
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1248
|
+
const near = [...steps, ...ends].find((n) => n.toLowerCase() === target.toLowerCase());
|
|
1249
|
+
const source = [...scope.stepNodeId].find(([, nodeId]) => nodeId === from.node)?.[0];
|
|
1250
|
+
refuse(from, new CompileRefusal('REF_UNKNOWN_TARGET', `.stepToRef('${target}') ${source !== undefined ? `after "${source}" ` : ''}names a step this flow does not have`
|
|
1251
|
+
+ `${near ? ` (did you mean "${near}"?)` : ''}. It takes the same name you passed to .step() or `
|
|
1252
|
+
+ `.terminate(), or the \`name\` of a \`.return({}, { name })\` End. Known steps: `
|
|
1253
|
+
+ `${steps.join(', ') || '(none)'}`
|
|
1254
|
+
+ `${ends.length ? `; named Ends: ${ends.join(', ')}` : ''}.`, near ? `.stepToRef('${near}')` : undefined));
|
|
1255
|
+
continue;
|
|
1158
1256
|
}
|
|
1159
1257
|
// Through the serializer's own `addEdge`, so the id comes from the one
|
|
1160
1258
|
// file-wide counter. A per-scope count (`edge_ref_${scope.edges.length}`)
|
|
@@ -1425,7 +1523,7 @@ function renameRefs(text, rename) {
|
|
|
1425
1523
|
* port paths (`.stepToList`, `.onError`) included: their steps are nodes of
|
|
1426
1524
|
* this scope too, so they need an id claimed with the rest.
|
|
1427
1525
|
*/
|
|
1428
|
-
function stepNamesOf(steps, out = [], terminates) {
|
|
1526
|
+
function stepNamesOf(steps, out = [], terminates, forks) {
|
|
1429
1527
|
for (const s of steps) {
|
|
1430
1528
|
// `return` carries a synthetic `name` but creates no node named after it (an
|
|
1431
1529
|
// End id is claimed as `end`); `stepToRef` has no `name` at all (it is one edge).
|
|
@@ -1434,30 +1532,32 @@ function stepNamesOf(steps, out = [], terminates) {
|
|
|
1434
1532
|
// A `.parallel` with a fork node claims that node's name too (#1310).
|
|
1435
1533
|
if (s.kind === 'parallel') {
|
|
1436
1534
|
const fork = parallelFork(s.options);
|
|
1437
|
-
if (fork)
|
|
1535
|
+
if (fork) {
|
|
1438
1536
|
out.push(fork.name);
|
|
1537
|
+
forks?.set(fork.name, s.name);
|
|
1538
|
+
}
|
|
1439
1539
|
}
|
|
1440
1540
|
// How many `.terminate`s hold each name, so a clash gets the right remedy (#1239).
|
|
1441
1541
|
if (s.kind === 'terminate' && terminates)
|
|
1442
1542
|
terminates.set(s.name, (terminates.get(s.name) ?? 0) + 1);
|
|
1443
1543
|
if (s.kind === 'stepToList')
|
|
1444
|
-
stepNamesOf(s.body, out, terminates);
|
|
1544
|
+
stepNamesOf(s.body, out, terminates, forks);
|
|
1445
1545
|
if (s.kind === 'branch') {
|
|
1446
|
-
stepNamesOf(s.then, out, terminates);
|
|
1447
|
-
stepNamesOf(s.otherwise, out, terminates);
|
|
1546
|
+
stepNamesOf(s.then, out, terminates, forks);
|
|
1547
|
+
stepNamesOf(s.otherwise, out, terminates, forks);
|
|
1448
1548
|
}
|
|
1449
1549
|
else if (s.kind === 'switch') {
|
|
1450
|
-
s.cases.forEach((c) => stepNamesOf(c.body, out, terminates));
|
|
1451
|
-
stepNamesOf(s.default ?? [], out, terminates);
|
|
1550
|
+
s.cases.forEach((c) => stepNamesOf(c.body, out, terminates, forks));
|
|
1551
|
+
stepNamesOf(s.default ?? [], out, terminates, forks);
|
|
1452
1552
|
}
|
|
1453
1553
|
else if (s.kind === 'stepSwitch')
|
|
1454
|
-
s.cases.forEach((c) => stepNamesOf(c.body, out, terminates));
|
|
1554
|
+
s.cases.forEach((c) => stepNamesOf(c.body, out, terminates, forks));
|
|
1455
1555
|
else if (s.kind === 'parallel')
|
|
1456
|
-
s.arms.forEach((a) => stepNamesOf(a, out, terminates));
|
|
1556
|
+
s.arms.forEach((a) => stepNamesOf(a, out, terminates, forks));
|
|
1457
1557
|
else if (s.kind === 'loop')
|
|
1458
|
-
stepNamesOf(s.body, out, terminates);
|
|
1558
|
+
stepNamesOf(s.body, out, terminates, forks);
|
|
1459
1559
|
else if (s.kind === 'doWhile')
|
|
1460
|
-
stepNamesOf(s.body, out, terminates);
|
|
1560
|
+
stepNamesOf(s.body, out, terminates, forks);
|
|
1461
1561
|
}
|
|
1462
1562
|
return out;
|
|
1463
1563
|
}
|
|
@@ -1515,6 +1615,73 @@ function safeNodeId(name) {
|
|
|
1515
1615
|
return name.replace(/[^A-Za-z0-9_]/g, '_') || 'node';
|
|
1516
1616
|
}
|
|
1517
1617
|
export function serialize(built, opts = {}) {
|
|
1618
|
+
const collect = opts.collect;
|
|
1619
|
+
if (!collect)
|
|
1620
|
+
return serializeFlow(built, opts);
|
|
1621
|
+
try {
|
|
1622
|
+
return serializeFlow(built, opts);
|
|
1623
|
+
}
|
|
1624
|
+
catch (error) {
|
|
1625
|
+
// A step's own refusal was taken by its catch (`emitStep`), so one that gets
|
|
1626
|
+
// here was thrown outside any one step: scope-level, and collection stops.
|
|
1627
|
+
// A throw that is no refusal is compile's COMPILE_FAILED; an internal SDK
|
|
1628
|
+
// error is passed on.
|
|
1629
|
+
if (!(error instanceof Error) || (!(error instanceof CompileRefusal) && error.message.startsWith('internal SDK error:')))
|
|
1630
|
+
throw error;
|
|
1631
|
+
const refusal = error instanceof CompileRefusal ? error : new CompileRefusal('COMPILE_FAILED', error.message);
|
|
1632
|
+
collect({ code: refusal.code, message: refusal.message, ...(refusal.suggestion !== undefined ? { suggestion: refusal.suggestion } : {}) });
|
|
1633
|
+
return undefined;
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
/**
|
|
1637
|
+
* The value an author wrote, as plain JSON: an expression as its `=js:` source,
|
|
1638
|
+
* a class instance as its own fields, a function dropped. Deterministic, so two
|
|
1639
|
+
* compiles of one unchanged spec give equal stand-ins (D5).
|
|
1640
|
+
*/
|
|
1641
|
+
function authoredValue(value, path = []) {
|
|
1642
|
+
if (value instanceof Expr)
|
|
1643
|
+
return `=js:${value.js}`;
|
|
1644
|
+
if (typeof value === 'function' || typeof value === 'symbol')
|
|
1645
|
+
return undefined;
|
|
1646
|
+
if (typeof value === 'bigint')
|
|
1647
|
+
return String(value);
|
|
1648
|
+
if (value === null || typeof value !== 'object')
|
|
1649
|
+
return value;
|
|
1650
|
+
if (path.includes(value))
|
|
1651
|
+
return '[circular]';
|
|
1652
|
+
const inner = [...path, value];
|
|
1653
|
+
if (Array.isArray(value))
|
|
1654
|
+
return value.map((v) => authoredValue(v, inner) ?? null);
|
|
1655
|
+
const out = {};
|
|
1656
|
+
for (const [k, v] of Object.entries(value)) {
|
|
1657
|
+
const a = authoredValue(v, inner);
|
|
1658
|
+
if (a !== undefined)
|
|
1659
|
+
out[k] = a;
|
|
1660
|
+
}
|
|
1661
|
+
return out;
|
|
1662
|
+
}
|
|
1663
|
+
const VERSION_RE = /^\d+\.\d+(\.\d+)?$/;
|
|
1664
|
+
/**
|
|
1665
|
+
* The opaque node collect mode emits for a step (or a root's trigger) whose
|
|
1666
|
+
* emission was refused: the node type and the spec as authored, under its node
|
|
1667
|
+
* id, with no definition (docs/CHECK_COMPILE_CONTRACT_DESIGN.md D5). A
|
|
1668
|
+
* comparison of two compiles finds it equal exactly when the spec, version and
|
|
1669
|
+
* name are.
|
|
1670
|
+
*/
|
|
1671
|
+
function standInNode(name, authored, version, id, parentId) {
|
|
1672
|
+
const spec = (authored ?? {});
|
|
1673
|
+
const kind = typeof spec.kind === 'string' ? spec.kind : undefined;
|
|
1674
|
+
const registered = kind !== undefined ? FLOW_NODE_TYPES.byKind(kind, spec) : undefined;
|
|
1675
|
+
return {
|
|
1676
|
+
id,
|
|
1677
|
+
type: registered?.type.typeName ?? bundledActionNodeType(spec) ?? kind ?? 'action',
|
|
1678
|
+
typeVersion: version !== undefined && VERSION_RE.test(version) ? version : '1.0.0',
|
|
1679
|
+
...(parentId ? { parentId } : {}),
|
|
1680
|
+
display: { label: name },
|
|
1681
|
+
inputs: { standIn: true, spec: authoredValue(spec) },
|
|
1682
|
+
};
|
|
1683
|
+
}
|
|
1684
|
+
function serializeFlow(built, opts) {
|
|
1518
1685
|
/**
|
|
1519
1686
|
* Definitions the flow carries (`.definitions()`, #1128), by `type@version` —
|
|
1520
1687
|
* the third source a pinned `{ version }` resolves from, after the bundle and
|
|
@@ -1537,8 +1704,9 @@ export function serialize(built, opts = {}) {
|
|
|
1537
1704
|
*/
|
|
1538
1705
|
const emittedDefs = new Map();
|
|
1539
1706
|
/**
|
|
1540
|
-
* The nodes a draft compile emitted unbound (their `BINDING_MISSING` finding)
|
|
1541
|
-
* they write no binding rows by design, so the
|
|
1707
|
+
* The nodes a draft compile emitted unbound (their `BINDING_MISSING` finding),
|
|
1708
|
+
* and collect mode's stand-ins: they write no binding rows by design, so the
|
|
1709
|
+
* placeholder check exempts them.
|
|
1542
1710
|
*/
|
|
1543
1711
|
const unboundDrafts = new Set();
|
|
1544
1712
|
/**
|
|
@@ -1572,16 +1740,18 @@ export function serialize(built, opts = {}) {
|
|
|
1572
1740
|
/**
|
|
1573
1741
|
* Refuse an `error` edge from a node whose definition has no error port: a
|
|
1574
1742
|
* contribution-lowered node (#943) or a typed node with a bundled definition
|
|
1575
|
-
* (#1011)
|
|
1743
|
+
* (#1011): ONERROR_NO_ERROR_PORT, which `check` reports from its serializer run.
|
|
1576
1744
|
*/
|
|
1577
|
-
function refuseErrorEdgeWithoutPort(nodeId, stepName) {
|
|
1745
|
+
function refuseErrorEdgeWithoutPort(scope, nodeId, stepName) {
|
|
1578
1746
|
const declared = declaredErrorPorts.get(nodeId);
|
|
1579
|
-
if (declared?.port === false)
|
|
1580
|
-
|
|
1747
|
+
if (declared?.port === false) {
|
|
1748
|
+
refuseOn(scope, stepName, new CompileRefusal('ONERROR_NO_ERROR_PORT', declaredErrorPortRefusal(stepName, declared.typeName, declared.version)));
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1581
1751
|
const contributed = contributionDefs.get(nodeId);
|
|
1582
1752
|
const def = contributed ?? bundledNodeDefs.get(nodeId);
|
|
1583
1753
|
if (def !== undefined && !definitionHasErrorPort(def)) {
|
|
1584
|
-
|
|
1754
|
+
refuseOn(scope, stepName, new CompileRefusal('ONERROR_NO_ERROR_PORT', errorPortRefusal(stepName, def, { bundled: contributed === undefined })));
|
|
1585
1755
|
}
|
|
1586
1756
|
}
|
|
1587
1757
|
/**
|
|
@@ -1645,24 +1815,29 @@ export function serialize(built, opts = {}) {
|
|
|
1645
1815
|
* gets them and one that does not is unaffected.
|
|
1646
1816
|
*/
|
|
1647
1817
|
const sidecars = opts.sidecars ?? [];
|
|
1648
|
-
/** The sidecar paths
|
|
1649
|
-
const explicitSidecars = new
|
|
1818
|
+
/** The sidecar paths an explicit `source` chose → who chose it (see `addSidecar`). */
|
|
1819
|
+
const explicitSidecars = new Map();
|
|
1650
1820
|
/**
|
|
1651
|
-
* Add one sidecar file.
|
|
1652
|
-
* another
|
|
1653
|
-
* the first: index A's node would read B's settings
|
|
1654
|
-
*
|
|
1655
|
-
*
|
|
1821
|
+
* Add one sidecar file. An agent or a resource whose explicit `source` names
|
|
1822
|
+
* a directory another one also writes would have the second file silently
|
|
1823
|
+
* replace the first: index A's node would read B's settings, or agent B would
|
|
1824
|
+
* run with A's configuration. That is refused (INLINE_AGENT_RESOURCE_SOURCE_DUP),
|
|
1825
|
+
* naming both; `owner` is the one whose `source` is explicit, as the refusal
|
|
1826
|
+
* names it (`inline agent step "answer"'s context index "SOP"`). The same file
|
|
1827
|
+
* twice (a subflow body emitted for two callers) is not a clash.
|
|
1656
1828
|
*/
|
|
1657
|
-
const addSidecar = (file,
|
|
1829
|
+
const addSidecar = (file, owner) => {
|
|
1658
1830
|
const earlier = sidecars.find((f) => f.path === file.path);
|
|
1831
|
+
const earlierOwner = explicitSidecars.get(file.path);
|
|
1659
1832
|
if (earlier !== undefined && JSON.stringify(earlier.json) !== JSON.stringify(file.json)
|
|
1660
|
-
&& (
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1833
|
+
&& (owner !== undefined || earlierOwner !== undefined)) {
|
|
1834
|
+
const names = [earlierOwner ?? 'another agent or resource', owner ?? 'another agent or resource'];
|
|
1835
|
+
throw new CompileRefusal('INLINE_AGENT_RESOURCE_SOURCE_DUP', `${names[0].charAt(0).toUpperCase()}${names[0].slice(1)} and ${names[1]} would both write "${file.path}" with `
|
|
1836
|
+
+ 'different content, so the second file would replace the first. Give each its own `source`, or omit '
|
|
1837
|
+
+ '`source` to derive one.');
|
|
1838
|
+
}
|
|
1839
|
+
if (owner !== undefined && earlierOwner === undefined)
|
|
1840
|
+
explicitSidecars.set(file.path, owner);
|
|
1666
1841
|
sidecars.push(file);
|
|
1667
1842
|
};
|
|
1668
1843
|
/**
|
|
@@ -1677,6 +1852,10 @@ export function serialize(built, opts = {}) {
|
|
|
1677
1852
|
return opts.agentSource ? opts.agentSource(source, stepName) : source;
|
|
1678
1853
|
};
|
|
1679
1854
|
const subflows = {};
|
|
1855
|
+
/** The calling steps of the subflow bodies being emitted, outermost first (`CollectedRefusal.subflows`). */
|
|
1856
|
+
const subflowPath = [];
|
|
1857
|
+
/** A refusal that escaped a subflow body in collect mode: scope-level, so no step's catch takes it. */
|
|
1858
|
+
let scopeRefusal;
|
|
1680
1859
|
const rootGlobals = emittableGlobals(built, `flow "${built.id}"`);
|
|
1681
1860
|
const root = newScope(rootGlobals.outputs, rootGlobals.vars, rootGlobals.backing, rootGlobals.backingReads);
|
|
1682
1861
|
root.openTails = built.openTails === true;
|
|
@@ -1737,15 +1916,15 @@ export function serialize(built, opts = {}) {
|
|
|
1737
1916
|
* End `x2`, and an unnamed `.return()` emitted first turned a named `end`
|
|
1738
1917
|
* into `end2`. Now:
|
|
1739
1918
|
*
|
|
1740
|
-
* - two Ends whose names give one id are refused (
|
|
1919
|
+
* - two Ends whose names give one id are refused (DUP_END_NAME);
|
|
1741
1920
|
* - an End whose name gives a step's id, the trigger's or an entry point's
|
|
1742
|
-
* is refused (
|
|
1921
|
+
* is refused (STEP_ID_COLLISION);
|
|
1743
1922
|
* - every other id is claimed here, so the ids the SDK makes up (`end`,
|
|
1744
1923
|
* `end2`, an agent's context node, ...) are the ones that move.
|
|
1745
1924
|
*
|
|
1746
|
-
* `check` reports
|
|
1747
|
-
*
|
|
1748
|
-
*
|
|
1925
|
+
* These are scope-level refusals: `check` reports the first, with no step.
|
|
1926
|
+
* A subflow COPY is the one place ids move: `copyPrefix` is set when the
|
|
1927
|
+
* copy is prefixed.
|
|
1749
1928
|
*/
|
|
1750
1929
|
function claimEndIds(scope, nodes, where, reserved, copyPrefix = '') {
|
|
1751
1930
|
const stepIds = new Map(nodes.steps.map((n) => [safeNodeId(n), n]));
|
|
@@ -1786,11 +1965,10 @@ export function serialize(built, opts = {}) {
|
|
|
1786
1965
|
* node id. It is never quietly renamed:
|
|
1787
1966
|
*
|
|
1788
1967
|
* - Two steps whose names give the same id (`.step('x')` twice, or `a b`
|
|
1789
|
-
* beside `a_b`) are refused
|
|
1790
|
-
*
|
|
1791
|
-
*
|
|
1792
|
-
*
|
|
1793
|
-
* `$vars.x` pointing at the first, with no word said.
|
|
1968
|
+
* beside `a_b`) are refused as DUP_STEP / STEP_ID_COLLISION, on every
|
|
1969
|
+
* path (`check` reports it from its serializer run, with no step: it is
|
|
1970
|
+
* thrown before any node is emitted). Renaming the second to `x2` left
|
|
1971
|
+
* `.stepToRef('x')` and `$vars.x` pointing at the first, with no word said.
|
|
1794
1972
|
* - In the flow itself, a step named like the trigger is refused the same
|
|
1795
1973
|
* way (STEP_ID_COLLISION).
|
|
1796
1974
|
* - A subflow body whose ids are already taken (a second copy of the same
|
|
@@ -1798,7 +1976,9 @@ export function serialize(built, opts = {}) {
|
|
|
1798
1976
|
* place ids move: the whole copy is prefixed with the calling step's name,
|
|
1799
1977
|
* and `rename` points its expressions at the prefixed ids.
|
|
1800
1978
|
*/
|
|
1801
|
-
function claimStepIds(scope, names, where, copyPrefix, alsoIds = [], terminates = new Map()
|
|
1979
|
+
function claimStepIds(scope, names, where, copyPrefix, alsoIds = [], terminates = new Map(),
|
|
1980
|
+
/** A `.parallel` fork's name → its parallel, for a fork named like a step. */
|
|
1981
|
+
forks = new Map()) {
|
|
1802
1982
|
const seen = new Map();
|
|
1803
1983
|
for (const n of names) {
|
|
1804
1984
|
const safe = n.replace(/[^A-Za-z0-9_]/g, '_') || 'node';
|
|
@@ -1813,9 +1993,12 @@ export function serialize(built, opts = {}) {
|
|
|
1813
1993
|
+ 'names that differ in letters, digits or "_".'
|
|
1814
1994
|
: t >= 2 ? `${where}: [DUP_STEP] ${terminateTwiceMessage(n)}`
|
|
1815
1995
|
: t === 1 ? `${where}: [DUP_STEP] ${terminateStepClashMessage(n)}`
|
|
1816
|
-
: `${where}: [DUP_STEP]
|
|
1817
|
-
+
|
|
1818
|
-
+
|
|
1996
|
+
: forks.has(n) ? `${where}: [DUP_STEP] the fork of parallel "${forks.get(n)}" and another step are both `
|
|
1997
|
+
+ `named "${n}". A step name is its node's id, so compile will not rename either one. Give one of `
|
|
1998
|
+
+ 'them another name.'
|
|
1999
|
+
: `${where}: [DUP_STEP] two steps are named "${n}". A step name is its node's id and \`.stepToRef\` / `
|
|
2000
|
+
+ `\`$vars\` name it, so compile will not rename the second one "${n}2". Give each step its own name; to `
|
|
2001
|
+
+ `send several paths to one step, keep one and end the others with .stepToRef('${n}').`);
|
|
1819
2002
|
}
|
|
1820
2003
|
seen.set(safe, n);
|
|
1821
2004
|
}
|
|
@@ -1837,20 +2020,35 @@ export function serialize(built, opts = {}) {
|
|
|
1837
2020
|
* The node id of an agent resource: the author's `nodeId` exactly as written
|
|
1838
2021
|
* (decompile writes the id the original file had, and a changed id is a
|
|
1839
2022
|
* dropped node plus an added one to merge), else `uid`'s derived one. A
|
|
1840
|
-
* `nodeId` another node already holds
|
|
1841
|
-
* the
|
|
2023
|
+
* `nodeId` another node already holds, or an entry point's id (claimed after
|
|
2024
|
+
* the body), is refused rather than renamed, for the same reason
|
|
2025
|
+
* (INLINE_AGENT_RESOURCE_NODE_ID). So is a derived id an explicit `nodeId`
|
|
2026
|
+
* already took: deriving `<id>2` instead would move a node the author's file
|
|
2027
|
+
* names by that id.
|
|
1842
2028
|
*/
|
|
1843
2029
|
function resourceNodeId(scope, nodeId, derivedBase, where) {
|
|
1844
|
-
if (nodeId === undefined)
|
|
2030
|
+
if (nodeId === undefined) {
|
|
2031
|
+
const owner = explicitResourceIds.get(derivedBase.replace(/[^A-Za-z0-9_]/g, '_') || 'node');
|
|
2032
|
+
if (owner !== undefined) {
|
|
2033
|
+
throw new CompileRefusal('INLINE_AGENT_RESOURCE_NODE_ID', `${owner} takes the \`nodeId\` ${JSON.stringify(derivedBase)}, which is the id compile derives for ${where}, `
|
|
2034
|
+
+ 'and compile will not rename either one. Give that resource another `nodeId`, or omit it to derive one.');
|
|
2035
|
+
}
|
|
1845
2036
|
return uid(scope, derivedBase);
|
|
1846
|
-
|
|
2037
|
+
}
|
|
2038
|
+
const entryPoint = (built.entryPoints ?? []).some((ep) => ep.id === nodeId);
|
|
2039
|
+
if (nodeId.trim() === '' || takenIds.has(nodeId) || entryPoint) {
|
|
1847
2040
|
throw new CompileRefusal('INLINE_AGENT_RESOURCE_NODE_ID', `${where}: \`nodeId\` ${JSON.stringify(nodeId)} `
|
|
1848
|
-
+ (nodeId.trim() === '' ? 'is blank.'
|
|
2041
|
+
+ (nodeId.trim() === '' ? 'is blank.'
|
|
2042
|
+
: entryPoint ? `is already the id of entry point "${nodeId}".`
|
|
2043
|
+
: 'is already the id of another node in this flow.')
|
|
1849
2044
|
+ ' Give the resource node an id no other node has, or omit `nodeId` to derive one.');
|
|
1850
2045
|
}
|
|
1851
2046
|
takenIds.add(nodeId);
|
|
2047
|
+
explicitResourceIds.set(nodeId, where);
|
|
1852
2048
|
return nodeId;
|
|
1853
2049
|
}
|
|
2050
|
+
/** Each explicit resource `nodeId` → the resource that took it (see `resourceNodeId`). */
|
|
2051
|
+
const explicitResourceIds = new Map();
|
|
1854
2052
|
/**
|
|
1855
2053
|
* The node id of an author's step: the one `claimStepIds` claimed for it,
|
|
1856
2054
|
* recorded so `.stepToRef('<step>')` can find it.
|
|
@@ -1883,6 +2081,10 @@ export function serialize(built, opts = {}) {
|
|
|
1883
2081
|
* `.stepToRef` rejoins now do come through here, #938).
|
|
1884
2082
|
*/
|
|
1885
2083
|
const hitlNodes = new Map();
|
|
2084
|
+
/** Human tasks with no per-outcome exits, and why (`FlowNodeContribution.noOutcomePorts`). */
|
|
2085
|
+
const noOutcomePorts = new Map();
|
|
2086
|
+
/** Per human task in `hitlNodes`, how its main path leaves it (`hitlExitViolation`'s `routing`). */
|
|
2087
|
+
const hitlRouting = new Map();
|
|
1886
2088
|
/**
|
|
1887
2089
|
* Human tasks with outcomes that set `legacyCompleted: true`: they leave on
|
|
1888
2090
|
* `completed` by the author's choice, so the guard exempts that one port
|
|
@@ -1903,9 +2105,26 @@ export function serialize(built, opts = {}) {
|
|
|
1903
2105
|
const hitlEndExits = [];
|
|
1904
2106
|
function addEdge(scope, from, toNode, toPort = 'input') {
|
|
1905
2107
|
const hitlNode = hitlNodes.get(from.node);
|
|
1906
|
-
const
|
|
1907
|
-
|
|
1908
|
-
|
|
2108
|
+
const none = from.port.startsWith('outcome-') ? noOutcomePorts.get(from.node) : undefined;
|
|
2109
|
+
const refused = none
|
|
2110
|
+
? {
|
|
2111
|
+
code: 'HITL_OUTCOME_PORTS_NONE',
|
|
2112
|
+
message: `Port "${from.port}" routes a per-outcome exit, but human task "${stepOfNode(scope, from.node) ?? from.node}" `
|
|
2113
|
+
+ `has none — ${none.why}, so its single exit is 'completed'.`,
|
|
2114
|
+
suggestion: none.suggestion,
|
|
2115
|
+
}
|
|
2116
|
+
: hitlNode
|
|
2117
|
+
? hitlExitViolation(hitlNode, from.port, legacyCompletedNodes.has(from.node), hitlRouting.get(from.node))
|
|
2118
|
+
: undefined;
|
|
2119
|
+
if (refused) {
|
|
2120
|
+
// On the task the edge leaves, not on the step it enters. In collect mode
|
|
2121
|
+
// the edge is not written, so no later guard trips on it.
|
|
2122
|
+
refuseOn(scope, stepOfNode(scope, from.node), new CompileRefusal(refused.code, `serialize: ${refused.message}`, refused.suggestion));
|
|
2123
|
+
return;
|
|
2124
|
+
}
|
|
2125
|
+
pushEdge(scope, from, toNode, toPort);
|
|
2126
|
+
}
|
|
2127
|
+
function pushEdge(scope, from, toNode, toPort = 'input') {
|
|
1909
2128
|
scope.edges.push({ id: `edge_${edgeSeq++}`, sourceNodeId: from.node, sourcePort: from.port, targetNodeId: toNode, targetPort: toPort });
|
|
1910
2129
|
}
|
|
1911
2130
|
let col = 0;
|
|
@@ -2025,13 +2244,22 @@ export function serialize(built, opts = {}) {
|
|
|
2025
2244
|
* defect that makes a subflow validate clean and then fault at run time with
|
|
2026
2245
|
* a null. It is emitted here, always, so it cannot be forgotten.
|
|
2027
2246
|
*/
|
|
2028
|
-
function emitSubflowBody(child, nodeId) {
|
|
2247
|
+
function emitSubflowBody(child, nodeId, stepName) {
|
|
2029
2248
|
const outer = carriedDefs;
|
|
2030
2249
|
carriedDefs = indexCarriedDefinitions(child.definitions ?? [], `subflow "${nodeId}" .definitions()`, new Map(outer));
|
|
2250
|
+
subflowPath.push(stepName);
|
|
2031
2251
|
try {
|
|
2032
2252
|
return emitSubflowBodyIn(child, nodeId);
|
|
2033
2253
|
}
|
|
2254
|
+
catch (error) {
|
|
2255
|
+
// Each step of the body had its own catch, so this refusal is the body's
|
|
2256
|
+
// scope-level one: the calling step's catch must not step over it.
|
|
2257
|
+
if (opts.collect && error instanceof CompileRefusal)
|
|
2258
|
+
scopeRefusal = error;
|
|
2259
|
+
throw error;
|
|
2260
|
+
}
|
|
2034
2261
|
finally {
|
|
2262
|
+
subflowPath.pop();
|
|
2035
2263
|
carriedDefs = outer;
|
|
2036
2264
|
}
|
|
2037
2265
|
}
|
|
@@ -2049,7 +2277,8 @@ export function serialize(built, opts = {}) {
|
|
|
2049
2277
|
// with its steps and move with it (#1253).
|
|
2050
2278
|
const childNodes = namedNodesOf(child.steps);
|
|
2051
2279
|
const childTerminates = new Map();
|
|
2052
|
-
const
|
|
2280
|
+
const childForks = new Map();
|
|
2281
|
+
const prefix = claimStepIds(scope, stepNamesOf(child.steps, [], childTerminates, childForks), `subflow "${nodeId}"`, `${nodeId}_`, childNodes.ends.map(safeNodeId), childTerminates, childForks);
|
|
2053
2282
|
claimEndIds(scope, childNodes, `subflow "${nodeId}"`, [], prefix);
|
|
2054
2283
|
const startId = uid(scope, `${nodeId}Start`);
|
|
2055
2284
|
// A CHILD's inputs are published as the child's own start node, whose id is
|
|
@@ -2083,7 +2312,7 @@ export function serialize(built, opts = {}) {
|
|
|
2083
2312
|
}
|
|
2084
2313
|
for (const vd of childGlobals.vars)
|
|
2085
2314
|
globals.push({ id: vd.name, name: vd.name, direction: 'inout', type: vd.type, ...varMeta(vd) });
|
|
2086
|
-
resolveRejoins(scope, addEdge);
|
|
2315
|
+
resolveRejoins(scope, addEdge, (from, refusal) => refuseOn(scope, stepOfNode(scope, from.node), refusal));
|
|
2087
2316
|
assertHitlExitInvariant(scope.nodes, scope.edges, legacyCompletedNodes);
|
|
2088
2317
|
typeAgentInputSchemas(scope, globals);
|
|
2089
2318
|
emitStickyNotes(child, scope, claimNoteId);
|
|
@@ -2103,101 +2332,108 @@ export function serialize(built, opts = {}) {
|
|
|
2103
2332
|
// node — its definition ships there with its `.v1def.json` sidecar — and it
|
|
2104
2333
|
// replaces the start node's TYPE, exactly as the scheduled trigger does. The
|
|
2105
2334
|
// node id stays `start`, so edges, `triggerNodeId` on flow inputs and the
|
|
2106
|
-
// `output` success port are untouched.
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
if (!opts.library)
|
|
2110
|
-
throw new CompileRefusal('LIBRARY_REQUIRED', `onEvent() needs a library (pass --library / set $FLOW_SDK_LIBRARY_JSON).`);
|
|
2111
|
-
const sub = built.trigger.subscription;
|
|
2112
|
-
const rc = opts.library.resolveEvent('trigger', sub.connector, sub.event, sub.version, sub.object);
|
|
2113
|
-
refuseUnverifiedByoa(opts, 'trigger', sub);
|
|
2114
|
-
return rc;
|
|
2115
|
-
})()
|
|
2116
|
-
: undefined;
|
|
2335
|
+
// `output` success port are untouched. In collect mode a refused trigger is
|
|
2336
|
+
// stood in (`emitRootNode`), so the steps after it are still judged.
|
|
2337
|
+
//
|
|
2117
2338
|
// The trigger node's id is `start` unless the flow renamed it. `input('x')`
|
|
2118
2339
|
// lowered eagerly to `$vars.start.output.x`, so a rename is applied by the
|
|
2119
2340
|
// same `renameRefs` pass that rewrites step names (see `rename.nodeIds`).
|
|
2120
2341
|
const triggerId = built.triggerId ?? 'start';
|
|
2121
|
-
|
|
2122
|
-
|
|
2123
|
-
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
requestedVersion: scheduledVersion(built.trigger.inputs, built.triggerOptions?.version),
|
|
2152
|
-
})
|
|
2153
|
-
: built.trigger?.kind === 'form'
|
|
2154
|
-
? makeNode(NODE_TYPE.formTrigger, triggerId, rootLabel, { requestedVersion: built.triggerOptions?.version })
|
|
2155
|
-
: built.trigger?.kind === 'conversation'
|
|
2156
|
-
? makeNode(NODE_TYPE.conversationTrigger, triggerId, rootLabel, { requestedVersion: built.triggerOptions?.version })
|
|
2157
|
-
: built.trigger?.kind === 'voice'
|
|
2158
|
-
? makeNode(NODE_TYPE.voiceTrigger, triggerId, rootLabel, { requestedVersion: built.triggerOptions?.version })
|
|
2159
|
-
: makeNode(NODE_TYPE.trigger, triggerId, rootLabel, { requestedVersion: built.triggerOptions?.version });
|
|
2160
|
-
if (!customTrigger) {
|
|
2161
|
-
trigger.inputs = eventTriggerRc && built.trigger?.kind === 'event'
|
|
2162
|
-
? {
|
|
2163
|
-
// The deployed shape: 38 of 39 archived instances carry `entryPointId`,
|
|
2164
|
-
// 28 carry `isDefaultEntryPoint: true`, and the subscription lives in
|
|
2165
|
-
// `detail` (see eventDetail).
|
|
2166
|
-
entryPointId: stableId(`${built.id}:start:entryPoint`),
|
|
2167
|
-
isDefaultEntryPoint: true,
|
|
2168
|
-
detail: eventDetail(eventTriggerRc, built.trigger.subscription, triggerId, opts, 'event trigger'),
|
|
2342
|
+
const trigger = emitRootNode('trigger', triggerId, built.trigger, built.triggerOptions?.version, () => {
|
|
2343
|
+
const eventTriggerRc = built.trigger?.kind === 'event'
|
|
2344
|
+
? (() => {
|
|
2345
|
+
const sub = built.trigger.subscription;
|
|
2346
|
+
if (!opts.library)
|
|
2347
|
+
throw libraryRequiredForEvent(opts, sub, built.triggerId ?? 'start', 'event trigger', 'onEvent()');
|
|
2348
|
+
const rc = resolveEvent(opts.library, 'trigger', sub, 'trigger');
|
|
2349
|
+
refuseUnverifiedByoa(opts, 'trigger', sub);
|
|
2350
|
+
return rc;
|
|
2351
|
+
})()
|
|
2352
|
+
: undefined;
|
|
2353
|
+
// A custom FlowTrigger subclass (no `kind`) compiles through its own
|
|
2354
|
+
// contribution: exact definition, author inputs, definition-owned identity.
|
|
2355
|
+
// When the manifest declares `model.entryPointId: true` (how the platform
|
|
2356
|
+
// marks entry points), the serializer mints the stable entry-point identity
|
|
2357
|
+
// exactly as it does for connector event triggers — an omitted one is the
|
|
2358
|
+
// classic validates-then-faults defect, so it cannot be forgotten here.
|
|
2359
|
+
const builtTrigger = built.trigger;
|
|
2360
|
+
const customTrigger = builtTrigger instanceof FlowTrigger && !('kind' in builtTrigger) ? builtTrigger : undefined;
|
|
2361
|
+
if (built.trigger !== undefined && !customTrigger)
|
|
2362
|
+
refuseUnknownTrigger(built.trigger, '.trigger()');
|
|
2363
|
+
if (customTrigger) {
|
|
2364
|
+
const contribution = contributionOf(customTrigger, built.triggerOptions?.version, 'trigger');
|
|
2365
|
+
const def = resolveContributionDefinition(contribution.definition, built.triggerOptions?.version, 'trigger');
|
|
2366
|
+
const node = makeNode(def.nodeType, triggerId, built.triggerOptions?.label ?? def.display?.label ?? 'Start', { def, requestedVersion: built.triggerOptions?.version });
|
|
2367
|
+
const inputs = renderDeep(contribution.inputs ?? {}, NO_RENAME);
|
|
2368
|
+
if (def.model?.entryPointId === true && inputs.entryPointId === undefined) {
|
|
2369
|
+
inputs.entryPointId = stableId(`${built.id}:start:entryPoint`);
|
|
2370
|
+
if (inputs.isDefaultEntryPoint === undefined)
|
|
2371
|
+
inputs.isDefaultEntryPoint = true;
|
|
2169
2372
|
}
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2178
|
-
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
|
|
2191
|
-
|
|
2192
|
-
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2373
|
+
node.inputs = inputs;
|
|
2374
|
+
hoistContributionBindings(def, contribution.bindings, 'trigger');
|
|
2375
|
+
root.nodes.push(node);
|
|
2376
|
+
}
|
|
2377
|
+
// The author's `.trigger(spec, { label })`; makeNode falls back to the definition's label.
|
|
2378
|
+
const rootLabel = built.triggerOptions?.label;
|
|
2379
|
+
const trigger = customTrigger
|
|
2380
|
+
? root.nodes[root.nodes.length - 1]
|
|
2381
|
+
: eventTriggerRc
|
|
2382
|
+
? makeNode(eventTriggerRc.nodeType, triggerId, rootLabel, { def: eventTriggerRc.v1def, requestedVersion: built.triggerOptions?.version })
|
|
2383
|
+
: built.trigger?.kind === 'scheduled'
|
|
2384
|
+
? makeNode(NODE_TYPE.scheduledTrigger, triggerId, rootLabel, {
|
|
2385
|
+
requestedVersion: scheduledVersion(built.trigger.inputs, built.triggerOptions?.version),
|
|
2386
|
+
})
|
|
2387
|
+
: built.trigger?.kind === 'form'
|
|
2388
|
+
? makeNode(NODE_TYPE.formTrigger, triggerId, rootLabel, { requestedVersion: built.triggerOptions?.version })
|
|
2389
|
+
: built.trigger?.kind === 'conversation'
|
|
2390
|
+
? makeNode(NODE_TYPE.conversationTrigger, triggerId, rootLabel, { requestedVersion: built.triggerOptions?.version })
|
|
2391
|
+
: built.trigger?.kind === 'voice'
|
|
2392
|
+
? makeNode(NODE_TYPE.voiceTrigger, triggerId, rootLabel, { requestedVersion: built.triggerOptions?.version })
|
|
2393
|
+
: makeNode(NODE_TYPE.trigger, triggerId, rootLabel, { requestedVersion: built.triggerOptions?.version });
|
|
2394
|
+
if (!customTrigger) {
|
|
2395
|
+
trigger.inputs = eventTriggerRc && built.trigger?.kind === 'event'
|
|
2396
|
+
? {
|
|
2397
|
+
// The deployed shape: 38 of 39 archived instances carry `entryPointId`,
|
|
2398
|
+
// 28 carry `isDefaultEntryPoint: true`, and the subscription lives in
|
|
2399
|
+
// `detail` (see eventDetail).
|
|
2400
|
+
entryPointId: stableId(`${built.id}:start:entryPoint`),
|
|
2401
|
+
isDefaultEntryPoint: true,
|
|
2402
|
+
detail: eventDetail(eventTriggerRc, built.trigger.subscription, triggerId, opts, 'event trigger'),
|
|
2403
|
+
}
|
|
2404
|
+
: built.trigger?.kind === 'scheduled'
|
|
2405
|
+
? scheduleInputs(built.trigger.inputs, triggerId)
|
|
2406
|
+
: built.trigger?.kind === 'form'
|
|
2407
|
+
? {
|
|
2408
|
+
// A GUID: publish keys the form's FormTrigger binding on it and skips
|
|
2409
|
+
// the form when it is not one.
|
|
2410
|
+
entryPointId: stableId(`${built.id}:${triggerId}:entryPoint`),
|
|
2411
|
+
schema: { fields: formTriggerFields(built.inputs), outcomes: [] },
|
|
2412
|
+
}
|
|
2413
|
+
: built.trigger?.kind === 'conversation' || built.trigger?.kind === 'voice'
|
|
2414
|
+
// Both are entry points (`model.entryPointId: true`) with nothing else
|
|
2415
|
+
// authored: what the platform started — the conversation, or the live
|
|
2416
|
+
// call's `callContext` — is the trigger's own declared output.
|
|
2417
|
+
? { entryPointId: triggerId }
|
|
2418
|
+
: {};
|
|
2419
|
+
}
|
|
2420
|
+
if (eventTriggerRc && built.trigger?.kind === 'event') {
|
|
2421
|
+
// An event trigger's def declares the same connection binding names a
|
|
2422
|
+
// mid-flow connector's does — close them (G-19; see connectionResourceBindings).
|
|
2423
|
+
const sub = built.trigger.subscription;
|
|
2424
|
+
const { entries } = connectionResourceBindings({
|
|
2425
|
+
def: eventTriggerRc.v1def,
|
|
2426
|
+
connectorKey: eventTriggerRc.connectorKey,
|
|
2427
|
+
connectionId: resolveBinding(opts, sub.connection, 'connection', 'event trigger', eventTarget(sub)),
|
|
2428
|
+
folderKey: resolveBinding(opts, sub.folder, 'folder', 'event trigger', eventTarget(sub)),
|
|
2429
|
+
}, resourceBindings);
|
|
2430
|
+
for (const b of entries)
|
|
2431
|
+
resourceBindings.set(b.id, b);
|
|
2432
|
+
}
|
|
2433
|
+
if (!customTrigger)
|
|
2434
|
+
root.nodes.push(trigger);
|
|
2435
|
+
return trigger;
|
|
2436
|
+
});
|
|
2201
2437
|
function successPort(type) {
|
|
2202
2438
|
if (type === NODE_TYPE.loop)
|
|
2203
2439
|
return 'success';
|
|
@@ -2216,7 +2452,7 @@ export function serialize(built, opts = {}) {
|
|
|
2216
2452
|
*/
|
|
2217
2453
|
function emitContribution(spec, stepName, requestedVersion, tails, scope, parentId) {
|
|
2218
2454
|
const where = `step "${stepName}"`;
|
|
2219
|
-
const contribution = spec
|
|
2455
|
+
const contribution = contributionOf(spec, requestedVersion, where);
|
|
2220
2456
|
const def = resolveContributionDefinition(contribution.definition, requestedVersion, where);
|
|
2221
2457
|
// A connector definition reached this path only through a raw or hand-written
|
|
2222
2458
|
// contribution — `connector()` never does — and would be emitted without
|
|
@@ -2269,10 +2505,19 @@ export function serialize(built, opts = {}) {
|
|
|
2269
2505
|
// edge out of it is held to that rule (`hitlExitViolation`, in addEdge), and
|
|
2270
2506
|
// the step list treats its tails as outcome edges. A rawNode of the type is
|
|
2271
2507
|
// not one: it routes by the port lists its author wrote.
|
|
2508
|
+
if (contribution.noOutcomePorts)
|
|
2509
|
+
noOutcomePorts.set(id, contribution.noOutcomePorts);
|
|
2272
2510
|
if (o.trackOutcomes && contribution.legacyCompleted)
|
|
2273
2511
|
legacyCompletedNodes.add(id);
|
|
2274
|
-
else if (o.trackOutcomes && OUTCOME_HANDLE_NODE_TYPES.has(o.typeName))
|
|
2512
|
+
else if (o.trackOutcomes && OUTCOME_HANDLE_NODE_TYPES.has(o.typeName)) {
|
|
2275
2513
|
hitlNodes.set(id, node);
|
|
2514
|
+
// How its main path leaves it: every outcome (a fan-out), or the primary one.
|
|
2515
|
+
const port = contribution.port;
|
|
2516
|
+
if (contribution.fanOut)
|
|
2517
|
+
hitlRouting.set(id, 'every-outcome');
|
|
2518
|
+
else if (port?.startsWith('outcome-'))
|
|
2519
|
+
hitlRouting.set(id, { primary: port });
|
|
2520
|
+
}
|
|
2276
2521
|
for (const a of contribution.attach ?? []) {
|
|
2277
2522
|
const def = a.definition;
|
|
2278
2523
|
const rnode = makeNode(def.nodeType, a.id, a.label, { parentId: o.parentId, def });
|
|
@@ -2367,7 +2612,7 @@ export function serialize(built, opts = {}) {
|
|
|
2367
2612
|
resourceBindings.set(b.id, b);
|
|
2368
2613
|
},
|
|
2369
2614
|
agentSource: (carried) => agentFolder(carried, step.name),
|
|
2370
|
-
sidecar: (file,
|
|
2615
|
+
sidecar: (file, owner) => addSidecar(file, owner === true ? 'another agent or resource' : owner === false ? undefined : owner),
|
|
2371
2616
|
typeAgentInputs: (schema, refs) => { scope.agentSchemas.push({ schema: schema, refs: [...refs] }); },
|
|
2372
2617
|
resourceNodeId: (explicit, seed, at) => resourceNodeId(scope, explicit, seed, at),
|
|
2373
2618
|
emitted: (typeName, v) => emittedDefs.get(`${typeName}@${v}`),
|
|
@@ -2484,6 +2729,169 @@ export function serialize(built, opts = {}) {
|
|
|
2484
2729
|
rendered,
|
|
2485
2730
|
});
|
|
2486
2731
|
}
|
|
2732
|
+
/**
|
|
2733
|
+
* What a throw out of one node's emission is, in collect mode: a refusal as
|
|
2734
|
+
* it is, and any other throw (a `TypeError` on a spec the serializer has no
|
|
2735
|
+
* refusal for) as COMPILE_FAILED, which is what `compile` reports for it. An
|
|
2736
|
+
* `internal SDK error:`, and a subflow body's scope-level refusal, are passed on.
|
|
2737
|
+
*/
|
|
2738
|
+
function stepRefusal(error) {
|
|
2739
|
+
if (error === scopeRefusal)
|
|
2740
|
+
throw error;
|
|
2741
|
+
if (error instanceof CompileRefusal)
|
|
2742
|
+
return error;
|
|
2743
|
+
if (!(error instanceof Error) || error.message.startsWith('internal SDK error:'))
|
|
2744
|
+
throw error;
|
|
2745
|
+
return new CompileRefusal('COMPILE_FAILED', error.message);
|
|
2746
|
+
}
|
|
2747
|
+
/** Report `refusal` to collect mode, on `step` (node `nodeId`) when it has one. */
|
|
2748
|
+
function record(refusal, step, nodeId) {
|
|
2749
|
+
opts.collect?.({
|
|
2750
|
+
code: refusal.code, message: refusal.message,
|
|
2751
|
+
...(refusal.suggestion !== undefined ? { suggestion: refusal.suggestion } : {}),
|
|
2752
|
+
...(step !== undefined ? { step, ...(nodeId !== undefined ? { nodeId } : {}) } : {}),
|
|
2753
|
+
...(step !== undefined && subflowPath.length ? { subflows: [...subflowPath] } : {}),
|
|
2754
|
+
});
|
|
2755
|
+
}
|
|
2756
|
+
/**
|
|
2757
|
+
* Refuse at a site past a step's own emission (an edge out of it, a port list
|
|
2758
|
+
* on it, a join it feeds), on the step the refusal is about. Collect mode
|
|
2759
|
+
* records it there and goes on with the graph as authored, so later steps are
|
|
2760
|
+
* still judged; any other compile throws it. With no owning step it throws in
|
|
2761
|
+
* collect mode too: then it is scope-level.
|
|
2762
|
+
*/
|
|
2763
|
+
function refuseOn(scope, stepName, refusal) {
|
|
2764
|
+
if (!opts.collect || stepName === undefined)
|
|
2765
|
+
throw refusal;
|
|
2766
|
+
record(refusal, stepName, scope.stepNodeId.get(stepName) ?? scope.pending.get(stepName));
|
|
2767
|
+
}
|
|
2768
|
+
/** The step whose node `nodeId` is, in `scope`. */
|
|
2769
|
+
function stepOfNode(scope, nodeId) {
|
|
2770
|
+
for (const [name, id] of scope.stepNodeId)
|
|
2771
|
+
if (id === nodeId)
|
|
2772
|
+
return name;
|
|
2773
|
+
return undefined;
|
|
2774
|
+
}
|
|
2775
|
+
/**
|
|
2776
|
+
* Emit one root's trigger node (the flow's trigger, an entry point), standing
|
|
2777
|
+
* it in when its emission is refused in collect mode, as `emitStep` does for a
|
|
2778
|
+
* step: reported on `name`, and the steps after it are still judged.
|
|
2779
|
+
*/
|
|
2780
|
+
function emitRootNode(name, id, spec, version, emitNode) {
|
|
2781
|
+
if (!opts.collect)
|
|
2782
|
+
return emitNode();
|
|
2783
|
+
const mark = { nodes: root.nodes.length, resources: new Set(resourceBindings.keys()) };
|
|
2784
|
+
try {
|
|
2785
|
+
return emitNode();
|
|
2786
|
+
}
|
|
2787
|
+
catch (error) {
|
|
2788
|
+
const refusal = stepRefusal(error);
|
|
2789
|
+
record(refusal, name, id);
|
|
2790
|
+
root.nodes.length = mark.nodes;
|
|
2791
|
+
for (const key of [...resourceBindings.keys()])
|
|
2792
|
+
if (!mark.resources.has(key))
|
|
2793
|
+
resourceBindings.delete(key);
|
|
2794
|
+
unboundDrafts.add(id);
|
|
2795
|
+
const node = standInNode(name, spec, version, id, undefined);
|
|
2796
|
+
root.nodes.push(node);
|
|
2797
|
+
return node;
|
|
2798
|
+
}
|
|
2799
|
+
}
|
|
2800
|
+
/**
|
|
2801
|
+
* Emit one action step and `finish` it (its updates and label). In collect
|
|
2802
|
+
* mode (`SerializeOpts.collect`) this is the per-step catch: a refusal is
|
|
2803
|
+
* reported on the step, everything the step had emitted is undone, and a
|
|
2804
|
+
* stand-in node takes its place, wired to the same tails.
|
|
2805
|
+
*/
|
|
2806
|
+
function emitStep(step, tails, scope, parentId, finish) {
|
|
2807
|
+
const collect = opts.collect;
|
|
2808
|
+
if (!collect) {
|
|
2809
|
+
const out = emitAction(step, tails, scope, parentId);
|
|
2810
|
+
finish();
|
|
2811
|
+
return out;
|
|
2812
|
+
}
|
|
2813
|
+
const mark = {
|
|
2814
|
+
nodes: scope.nodes.length, edges: scope.edges.length, pendingEdges: scope.pendingEdges.length,
|
|
2815
|
+
agentSchemas: scope.agentSchemas.length, sidecars: sidecars.length,
|
|
2816
|
+
};
|
|
2817
|
+
try {
|
|
2818
|
+
const out = emitAction(step, tails, scope, parentId);
|
|
2819
|
+
finish();
|
|
2820
|
+
return out;
|
|
2821
|
+
}
|
|
2822
|
+
catch (error) {
|
|
2823
|
+
const refusal = stepRefusal(error);
|
|
2824
|
+
const id = scope.stepNodeId.get(step.name) ?? stepUid(scope, step.name);
|
|
2825
|
+
record(refusal, step.name, id);
|
|
2826
|
+
scope.nodes.length = mark.nodes;
|
|
2827
|
+
scope.edges.length = mark.edges;
|
|
2828
|
+
scope.pendingEdges.length = mark.pendingEdges;
|
|
2829
|
+
scope.agentSchemas.length = mark.agentSchemas;
|
|
2830
|
+
for (const f of sidecars.splice(mark.sidecars)) {
|
|
2831
|
+
if (!sidecars.some((kept) => kept.path === f.path))
|
|
2832
|
+
explicitSidecars.delete(f.path);
|
|
2833
|
+
}
|
|
2834
|
+
delete scope.variableUpdates[id];
|
|
2835
|
+
delete subflows[id];
|
|
2836
|
+
// An error port recorded before the throw is the type's answer; keep it for the stand-in.
|
|
2837
|
+
const knownPort = declaredErrorPorts.get(id);
|
|
2838
|
+
for (const byId of [contributionDefs, bundledNodeDefs, declaredErrorPorts, hitlNodes, hitlRouting, noOutcomePorts])
|
|
2839
|
+
byId.delete(id);
|
|
2840
|
+
legacyCompletedNodes.delete(id);
|
|
2841
|
+
capsOpenTail.delete(id);
|
|
2842
|
+
routerNodes.delete(step.name);
|
|
2843
|
+
// No binding rows and no definition: the placeholder check passes it, as a draft's.
|
|
2844
|
+
unboundDrafts.add(id);
|
|
2845
|
+
scope.nodes.push(standInNode(step.name, step.spec, step.options?.version, id, parentId));
|
|
2846
|
+
// Its error port is its type's, so an `.onError` after it is still judged.
|
|
2847
|
+
const port = standInErrorPort(step) ?? knownPort;
|
|
2848
|
+
if (port)
|
|
2849
|
+
declaredErrorPorts.set(id, port);
|
|
2850
|
+
// Wired as any node is: an edge a human task does not have is refused on
|
|
2851
|
+
// the task (`addEdge`, collect mode) and not written.
|
|
2852
|
+
for (const t of tails) {
|
|
2853
|
+
try {
|
|
2854
|
+
addEdge(scope, t, id);
|
|
2855
|
+
}
|
|
2856
|
+
catch (edgeError) {
|
|
2857
|
+
if (!(edgeError instanceof CompileRefusal))
|
|
2858
|
+
throw edgeError;
|
|
2859
|
+
record(edgeError);
|
|
2860
|
+
}
|
|
2861
|
+
}
|
|
2862
|
+
return [{ node: id, port: 'output' }];
|
|
2863
|
+
}
|
|
2864
|
+
}
|
|
2865
|
+
/**
|
|
2866
|
+
* Whether a refused step's node type has an error port, as its emission would
|
|
2867
|
+
* have recorded it: a registered type's declaration or bundled definition.
|
|
2868
|
+
* `undefined` when the type cannot say (a custom node, a pin no version
|
|
2869
|
+
* resolves) and nothing was recorded before the throw: its edge is not judged.
|
|
2870
|
+
*/
|
|
2871
|
+
function standInErrorPort(step) {
|
|
2872
|
+
const spec = step.spec;
|
|
2873
|
+
const entry = typeof spec.kind === 'string' ? FLOW_NODE_TYPES.byKind(spec.kind, spec) : undefined;
|
|
2874
|
+
if (!entry)
|
|
2875
|
+
return undefined;
|
|
2876
|
+
try {
|
|
2877
|
+
const resolved = resolveVersionCarried(entry.type, spec, step.options?.version, `step "${step.name}"`, carriedFor);
|
|
2878
|
+
const impl = resolved.impl;
|
|
2879
|
+
if (resolved.carried !== undefined) {
|
|
2880
|
+
return { port: definitionHasErrorPort(resolved.carried), typeName: impl.typeName, version: resolved.version };
|
|
2881
|
+
}
|
|
2882
|
+
if (impl.definition === 'bundled') {
|
|
2883
|
+
const def = bundledDefinition(impl.typeName, resolved.version);
|
|
2884
|
+
return def === undefined ? undefined : { port: definitionHasErrorPort(def), typeName: def.nodeType, version: def.version };
|
|
2885
|
+
}
|
|
2886
|
+
const port = typeof impl.errorPort === 'function' ? impl.errorPort(spec, undefined) : impl.errorPort === true;
|
|
2887
|
+
return { port, typeName: impl.typeName, version: resolved.version };
|
|
2888
|
+
}
|
|
2889
|
+
catch (error) {
|
|
2890
|
+
if (error instanceof CompileRefusal)
|
|
2891
|
+
return undefined;
|
|
2892
|
+
throw error;
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2487
2895
|
function emitAction(step, tails, scope, parentId) {
|
|
2488
2896
|
const spec = step.spec;
|
|
2489
2897
|
// A registered node type serializes itself; every other kind falls through to
|
|
@@ -2515,17 +2923,24 @@ export function serialize(built, opts = {}) {
|
|
|
2515
2923
|
addEdge(scope, t, id);
|
|
2516
2924
|
// Emit the body AFTER the node exists, so a child that itself calls a
|
|
2517
2925
|
// subflow registers its own entry in the same flat map.
|
|
2518
|
-
subflows[id] = emitSubflowBody(spec.child, id);
|
|
2926
|
+
subflows[id] = emitSubflowBody(spec.child, id, step.name);
|
|
2519
2927
|
return [{ node: id, port: successPort(NODE_TYPE.subflow) }];
|
|
2520
2928
|
}
|
|
2521
2929
|
if (spec.kind === 'waitEvent') {
|
|
2522
|
-
if (!opts.library)
|
|
2523
|
-
throw new CompileRefusal('LIBRARY_REQUIRED', `waitForEvent "${step.name}" needs a library (pass --library / set $FLOW_SDK_LIBRARY_JSON).`);
|
|
2524
2930
|
const sub = spec.subscription;
|
|
2525
|
-
|
|
2931
|
+
// An event wait resumes or it does not: it has no error port to route.
|
|
2932
|
+
// Recorded before anything can refuse the step, so a stand-in keeps it.
|
|
2933
|
+
declaredErrorPorts.set(scope.pending.get(step.name) ?? step.name, {
|
|
2934
|
+
port: false, typeName: `uipath.connector.event.${sub?.connector}.${sub?.event}`, version: step.options?.version ?? sub?.version ?? '1.0.0',
|
|
2935
|
+
});
|
|
2936
|
+
if (!opts.library) {
|
|
2937
|
+
throw libraryRequiredForEvent(opts, sub, scope.pending.get(step.name) ?? step.name, `waitForEvent "${step.name}"`, `waitForEvent "${step.name}"`);
|
|
2938
|
+
}
|
|
2939
|
+
const rc = resolveEvent(opts.library, 'event', sub, `waitForEvent "${step.name}"`);
|
|
2526
2940
|
refuseUnverifiedByoa(opts, `waitForEvent "${step.name}"`, sub);
|
|
2527
2941
|
const id = stepUid(scope, step.name);
|
|
2528
2942
|
const node = makeNode(rc.nodeType, id, step.name, { parentId, requestedVersion: step.options?.version, def: rc.v1def });
|
|
2943
|
+
declaredErrorPorts.set(id, { port: false, typeName: rc.nodeType, version: String(rc.v1def?.version ?? node.typeVersion) });
|
|
2529
2944
|
node.inputs = { detail: eventDetail(rc, sub, id, opts, `waitForEvent "${step.name}"`) };
|
|
2530
2945
|
// Same connection-binding closure as the connector branch (G-19).
|
|
2531
2946
|
const { entries: waitConnBindings } = connectionResourceBindings({
|
|
@@ -2594,12 +3009,11 @@ export function serialize(built, opts = {}) {
|
|
|
2594
3009
|
/**
|
|
2595
3010
|
* Refuse a side exit on a human task's port that the step's tails already
|
|
2596
3011
|
* continue from: that would put two edges on one port (flow-builder-sdk#741).
|
|
2597
|
-
* After a fan-out task that is EVERY outcome port.
|
|
2598
|
-
* as HITL_OUTCOME_PORTS_OFF; this is the compile-side half. A
|
|
3012
|
+
* After a fan-out task that is EVERY outcome port (HITL_OUTCOME_PORTS_OFF). A
|
|
2599
3013
|
* `legacyCompleted` task counts too: its main path leaves on `completed`, so a
|
|
2600
3014
|
* side route there is the same second edge.
|
|
2601
3015
|
*/
|
|
2602
|
-
function refuseRewire(method, port, source, tails) {
|
|
3016
|
+
function refuseRewire(scope, method, port, source, tails) {
|
|
2603
3017
|
const node = tails[0]?.node ?? '';
|
|
2604
3018
|
if (!(hitlNodes.has(node) || legacyCompletedNodes.has(node)) || !tails.some((t) => t.port === port))
|
|
2605
3019
|
return;
|
|
@@ -2607,12 +3021,14 @@ export function serialize(built, opts = {}) {
|
|
|
2607
3021
|
// outcome, which is check's HITL_OUTCOME_PORTS_OFF. One tail: that port
|
|
2608
3022
|
// already continues the main path (a routed task's primary outcome, or a
|
|
2609
3023
|
// legacyCompleted task's `completed`), and outcomePorts would not help.
|
|
2610
|
-
|
|
3024
|
+
refuseOn(scope, source, new CompileRefusal(tails.length > 1 ? 'HITL_OUTCOME_PORTS_OFF' : 'HITL_PORT_ALREADY_WIRED', tails.length > 1
|
|
2611
3025
|
? `${method}('${port}', …) after human task "${source}": every outcome already continues to the next `
|
|
2612
3026
|
+ `step, '${port}' included, because this task does not route per outcome. Add outcomePorts: true `
|
|
2613
3027
|
+ `(the first outcome then continues and the others are routed), or route every outcome with .stepSwitch.`
|
|
2614
3028
|
: `${method}('${port}', …) after human task "${source}": '${port}' already continues the main path, so `
|
|
2615
|
-
+ `routing it here too would put two edges on one port (flow-builder-sdk#741)
|
|
3029
|
+
+ `routing it here too would put two edges on one port (flow-builder-sdk#741).`, tails.length > 1
|
|
3030
|
+
? 'outcomePorts: true (then the first outcome continues and the others are routed), or route every outcome with .stepSwitch'
|
|
3031
|
+
: undefined));
|
|
2616
3032
|
}
|
|
2617
3033
|
/**
|
|
2618
3034
|
* A step list hanging off a named PORT of the step just emitted.
|
|
@@ -2635,7 +3051,7 @@ export function serialize(built, opts = {}) {
|
|
|
2635
3051
|
if (tails.length === 0 || tails.some((t) => t.node !== tails[0].node)) {
|
|
2636
3052
|
throw new Error(`internal SDK error: .stepToList('${step.port}', …) after step "${source}": expected the step to leave from exactly one node, got ${tails.length} tail(s).`);
|
|
2637
3053
|
}
|
|
2638
|
-
refuseRewire('.stepToList', step.port, source, tails);
|
|
3054
|
+
refuseRewire(scope, '.stepToList', step.port, source, tails);
|
|
2639
3055
|
const nodeId = tails[0].node;
|
|
2640
3056
|
const node = scope.nodes.find((n) => n.id === nodeId);
|
|
2641
3057
|
if (!node)
|
|
@@ -2643,7 +3059,7 @@ export function serialize(built, opts = {}) {
|
|
|
2643
3059
|
// Only the error port carries this: without it the platform routes but Studio
|
|
2644
3060
|
// Web HIDES the handle, so no human can see the handler.
|
|
2645
3061
|
if (step.port === 'error') {
|
|
2646
|
-
refuseErrorEdgeWithoutPort(nodeId, source);
|
|
3062
|
+
refuseErrorEdgeWithoutPort(scope, nodeId, source);
|
|
2647
3063
|
node.inputs = { ...(node.inputs ?? {}), errorHandlingEnabled: true };
|
|
2648
3064
|
}
|
|
2649
3065
|
const handlerTails = emit(handler, [{ node: nodeId, port: step.port }], scope, parentId, loopExits);
|
|
@@ -2709,14 +3125,12 @@ export function serialize(built, opts = {}) {
|
|
|
2709
3125
|
// follow: nothing continues from it, so a step here would hang off no edge.
|
|
2710
3126
|
const router = lastAction !== undefined && tails.length === 0 ? routerNodes.get(lastAction) : undefined;
|
|
2711
3127
|
if (router !== undefined && step.kind !== 'stepToList' && step.kind !== 'stepToRef') {
|
|
2712
|
-
|
|
3128
|
+
refuseOn(scope, lastAction, new CompileRefusal('TEMPLATED_EXITS_ONLY', `step "${lastAction}": its definition declares only templated exits (such as case-{item.id}), so no `
|
|
2713
3129
|
+ `path continues from it and ${'name' in step ? `"${step.name}"` : `.${step.kind}()`} after it would `
|
|
2714
|
-
+ `have no incoming edge. Route each exit with .stepToList('<port>', …), e.g. .stepToList('case-<id>', …).`);
|
|
3130
|
+
+ `have no incoming edge. Route each exit with .stepToList('<port>', …), e.g. .stepToList('case-<id>', …).`));
|
|
2715
3131
|
}
|
|
2716
3132
|
if (step.kind === 'action') {
|
|
2717
|
-
tails =
|
|
2718
|
-
applyUpdates(step);
|
|
2719
|
-
applyLabel(step);
|
|
3133
|
+
tails = emitStep(step, tails, scope, parentId, () => { applyUpdates(step); applyLabel(step); });
|
|
2720
3134
|
lastAction = step.name;
|
|
2721
3135
|
// A fan-out human task's `action: 'End'` outcomes end the run here: the
|
|
2722
3136
|
// flow runtime never reads `action`, so only an End node can. When the
|
|
@@ -2750,11 +3164,13 @@ export function serialize(built, opts = {}) {
|
|
|
2750
3164
|
// no routable fan-out (a human task pinned to 1.0, a variant, an http
|
|
2751
3165
|
// with no branches) must refuse rather than emit a node and then
|
|
2752
3166
|
// discover it has one port. `stepExits` carries the reason.
|
|
2753
|
-
const
|
|
2754
|
-
if (!hasExits(
|
|
2755
|
-
|
|
2756
|
-
+ (exits.suggestion ? ` ${exits.suggestion}` : ''));
|
|
3167
|
+
const declaredExits = stepExits(step.spec, step.options?.version);
|
|
3168
|
+
if (!hasExits(declaredExits)) {
|
|
3169
|
+
refuseOn(scope, step.name, new CompileRefusal('STEP_SWITCH_NO_EXITS', `.stepSwitch("${step.name}"): ${declaredExits.reason}, so there are no exits to route.`, declaredExits.suggestion));
|
|
2757
3170
|
}
|
|
3171
|
+
// In collect mode, past a refusal, each arm is still emitted (from the
|
|
3172
|
+
// port its value names), so the steps in it are judged.
|
|
3173
|
+
const exits = hasExits(declaredExits) ? declaredExits : [];
|
|
2758
3174
|
const action = {
|
|
2759
3175
|
kind: 'action', name: step.name, spec: step.spec,
|
|
2760
3176
|
...(step.options ? { options: step.options } : {}),
|
|
@@ -2763,9 +3179,7 @@ export function serialize(built, opts = {}) {
|
|
|
2763
3179
|
// primary outcome, http's `default`. Discarded on purpose: every exit is
|
|
2764
3180
|
// an arm here, so that port is wired by the arm that names it (and
|
|
2765
3181
|
// wiring both is flow-builder-sdk#741).
|
|
2766
|
-
|
|
2767
|
-
applyUpdates(action);
|
|
2768
|
-
applyLabel(action);
|
|
3182
|
+
emitStep(action, tails, scope, parentId, () => { applyUpdates(action); applyLabel(action); });
|
|
2769
3183
|
const nodeId = scope.stepNodeId.get(step.name);
|
|
2770
3184
|
if (!nodeId)
|
|
2771
3185
|
throw new Error(`internal SDK error: .stepSwitch("${step.name}"): emitted node not found.`);
|
|
@@ -2775,11 +3189,13 @@ export function serialize(built, opts = {}) {
|
|
|
2775
3189
|
// in `.return()` here, an outcome with no arm below.
|
|
2776
3190
|
const stopped = [];
|
|
2777
3191
|
for (const c of step.cases) {
|
|
2778
|
-
const
|
|
2779
|
-
if (!
|
|
2780
|
-
|
|
2781
|
-
|
|
3192
|
+
const found = exits.find((e) => e.value === c.value);
|
|
3193
|
+
if (!found && hasExits(declaredExits)) {
|
|
3194
|
+
const nearest = nearestName(c.value, exits.map((e) => e.value));
|
|
3195
|
+
refuseOn(scope, step.name, new CompileRefusal('STEP_SWITCH_EXIT_UNKNOWN', `.stepSwitch("${step.name}") has an arm for ${JSON.stringify(c.value)}, which is not `
|
|
3196
|
+
+ `an exit it declares — those are ${exits.map((e) => JSON.stringify(e.value)).join(', ')}.`, nearest !== undefined ? `value: '${nearest}'` : undefined));
|
|
2782
3197
|
}
|
|
3198
|
+
const exit = found ?? { value: c.value, port: String(c.value) };
|
|
2783
3199
|
routed.add(exit.port);
|
|
2784
3200
|
// Arms CONVERGE, exactly as `.switch()`'s do: every arm that does not
|
|
2785
3201
|
// end in `.return()` contributes its tail, so the next step fans in
|
|
@@ -2899,9 +3315,9 @@ export function serialize(built, opts = {}) {
|
|
|
2899
3315
|
// The join waits for a token on EVERY incoming edge, and a fan-out human
|
|
2900
3316
|
// task's outcome edges are mutually exclusive — only one fires. So two
|
|
2901
3317
|
// of them straight into the join, or an End outcome beside a continuing
|
|
2902
|
-
// one, hang the run with no incident
|
|
2903
|
-
//
|
|
2904
|
-
//
|
|
3318
|
+
// one, hang the run with no incident: HITL_FAN_OUT_INTO_JOIN and
|
|
3319
|
+
// HITL_END_OUTCOME_IN_PARALLEL, worded from the step tree
|
|
3320
|
+
// (`flow-parallel-hitl.ts`). It fails closed.
|
|
2905
3321
|
// The fork, when the author names one (`{ fork }`, #1310): a merge node
|
|
2906
3322
|
// with one incoming edge and one edge per arm — the gateway Studio Web
|
|
2907
3323
|
// draws for a parallel split. The arms then leave from it instead of
|
|
@@ -2913,9 +3329,9 @@ export function serialize(built, opts = {}) {
|
|
|
2913
3329
|
// fan-out task's outcomes — must not both enter it: only one fires
|
|
2914
3330
|
// and the arms never start. Fails closed, as the join does.
|
|
2915
3331
|
if (tails.length > 1) {
|
|
2916
|
-
|
|
3332
|
+
refuseOn(scope, step.name, new CompileRefusal('PARALLEL_FORK_ALTERNATIVES', `.parallel("${step.name}"): its fork "${fork.name}" would receive ${tails.length} alternative edges `
|
|
2917
3333
|
+ `(${tails.map((t) => `${t.node}@${t.port}`).join(', ')}), and only one of them fires while the gateway `
|
|
2918
|
-
+ `waits for all of them. Add a step before the .parallel so the paths meet there first, or drop { fork }.`);
|
|
3334
|
+
+ `waits for all of them. Add a step before the .parallel so the paths meet there first, or drop { fork }.`));
|
|
2919
3335
|
}
|
|
2920
3336
|
const forkId = stepUid(scope, fork.name);
|
|
2921
3337
|
const forkNode = makeNode(NODE_TYPE.merge, forkId, fork.label ?? fork.name, { parentId, requestedVersion: fork.version ?? step.options?.version });
|
|
@@ -2927,8 +3343,13 @@ export function serialize(built, opts = {}) {
|
|
|
2927
3343
|
}
|
|
2928
3344
|
const armTails = [];
|
|
2929
3345
|
const endsBefore = hitlEndExits.length;
|
|
2930
|
-
for (const arm of step.arms) {
|
|
3346
|
+
for (const [armIndex, arm] of step.arms.entries()) {
|
|
2931
3347
|
const t = emit(arm, tails, scope, parentId, loopExits);
|
|
3348
|
+
// Worded from the step tree, which names the outcomes; this graph's tails decide.
|
|
3349
|
+
const fanIn = () => {
|
|
3350
|
+
const treeTails = hitlFanInto(arm);
|
|
3351
|
+
return treeTails && fanInRefusal(step.name, armIndex, arm, treeTails);
|
|
3352
|
+
};
|
|
2932
3353
|
// An arm hands the join ONE edge, or several that are alternatives —
|
|
2933
3354
|
// `.branch` / `.switch` / `.stepSwitch` arms, a fan-out task's
|
|
2934
3355
|
// outcomes — of which only one fires. Refused when any of them is a
|
|
@@ -2941,26 +3362,34 @@ export function serialize(built, opts = {}) {
|
|
|
2941
3362
|
// a complete `.stepSwitch` with nothing after it hands the join one
|
|
2942
3363
|
// edge per arm, and still only one arm runs.
|
|
2943
3364
|
const switchTail = outcomeTail ? undefined : t.find((x) => x.humanSwitch !== undefined);
|
|
3365
|
+
// Each is the human task's refusal, so it is reported on the task.
|
|
2944
3366
|
if (t.length >= 2 && switchTail) {
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
3367
|
+
const worded = fanIn();
|
|
3368
|
+
refuseOn(scope, worded?.task ?? switchTail.humanSwitch, new CompileRefusal('HITL_FAN_OUT_INTO_JOIN', worded?.message
|
|
3369
|
+
?? `.parallel("${step.name}"): an arm sends ${t.length} alternative edges into join "${step.name}" — `
|
|
3370
|
+
+ `among them the end of an arm of human task "${switchTail.humanSwitch}"'s .stepSwitch — and only one `
|
|
3371
|
+
+ `of them fires, while the join waits for all of them. Add a step at the end of the arm, after the `
|
|
3372
|
+
+ `.stepSwitch, so its arms meet there first, or move the task out of the .parallel.`, worded?.suggestion));
|
|
2949
3373
|
}
|
|
2950
|
-
if (t.length >= 2 && outcomeTail) {
|
|
2951
|
-
|
|
2952
|
-
|
|
2953
|
-
|
|
2954
|
-
|
|
3374
|
+
else if (t.length >= 2 && outcomeTail) {
|
|
3375
|
+
const worded = fanIn();
|
|
3376
|
+
refuseOn(scope, worded?.task ?? stepOfNode(scope, outcomeTail.node), new CompileRefusal('HITL_FAN_OUT_INTO_JOIN', worded?.message
|
|
3377
|
+
?? `.parallel("${step.name}"): an arm sends ${t.length} alternative edges into join "${step.name}" — `
|
|
3378
|
+
+ `among them outcome edge "${outcomeTail.port}" of human task "${outcomeTail.node}" — and only one `
|
|
3379
|
+
+ `of them fires, while the join waits for all of them. Add a step at the end of the arm so its paths `
|
|
3380
|
+
+ `meet there first, or move the task out of the .parallel.`, worded?.suggestion));
|
|
2955
3381
|
}
|
|
2956
3382
|
armTails.push(...t);
|
|
2957
3383
|
}
|
|
2958
3384
|
const starved = hitlEndExits.slice(endsBefore).find((x) => x.scope === scope && x.parentId === parentId);
|
|
2959
3385
|
if (starved && armTails.length > 0) {
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
3386
|
+
const hazard = step.arms.flatMap((arm) => hitlEndHazards(arm)).find((h) => h.step === starved.step);
|
|
3387
|
+
const worded = hazard && endOutcomeRefusal(step.name, hazard);
|
|
3388
|
+
refuseOn(scope, starved.step, new CompileRefusal('HITL_END_OUTCOME_IN_PARALLEL', worded?.message
|
|
3389
|
+
?? `.parallel("${step.name}"): human task "${starved.step}" ${starved.why} inside an arm, and join `
|
|
3390
|
+
+ `"${step.name}" would wait forever for the path that continues. Give every outcome a path that `
|
|
3391
|
+
+ `continues to the end of the arm (drop action: 'End', add the missing .stepSwitch arm, move the `
|
|
3392
|
+
+ `.return() after the .parallel), or move the task out of the .parallel.`, worded?.suggestion));
|
|
2964
3393
|
}
|
|
2965
3394
|
// When EVERY arm is terminal — a terminate, or an End — there is nothing
|
|
2966
3395
|
// left to join, and emitting a Merge would add a node with no incoming
|
|
@@ -3001,8 +3430,10 @@ export function serialize(built, opts = {}) {
|
|
|
3001
3430
|
const loopDef = definitionFor(NODE_TYPE.loop, version);
|
|
3002
3431
|
const refusal = loopVersionRefusal(step.name, o, usesBreak, loopDef);
|
|
3003
3432
|
if (refusal)
|
|
3004
|
-
|
|
3005
|
-
|
|
3433
|
+
refuseOn(scope, step.name, new CompileRefusal('LOOP_VERSION_UNSUPPORTED', refusal));
|
|
3434
|
+
// Past that refusal (collect mode) the body is emitted with the handles it
|
|
3435
|
+
// asks for, so a `.break()` in it is not refused a second time.
|
|
3436
|
+
const v24 = loopHasInnerHandles(version, loopDef) || refusal !== undefined;
|
|
3006
3437
|
const declared = loopDeclaredInputs(loopDef);
|
|
3007
3438
|
const id = stepUid(scope, step.name);
|
|
3008
3439
|
const node = makeNode(NODE_TYPE.loop, id, step.options?.label ?? step.label ?? step.name, { parentId, requestedVersion: version });
|
|
@@ -3073,8 +3504,7 @@ export function serialize(built, opts = {}) {
|
|
|
3073
3504
|
// A Terminate is a `.stepToRef` target (its name is a step name), so a
|
|
3074
3505
|
// second `.terminate` asking for a name this scope already holds is
|
|
3075
3506
|
// refused, not renamed `<name>2`: that rename turned one Terminate two
|
|
3076
|
-
// paths share into two Terminates with no word said (#1239).
|
|
3077
|
-
// reports it first as DUP_STEP; this is the backstop for `--no-check`.
|
|
3507
|
+
// paths share into two Terminates with no word said (#1239): DUP_STEP.
|
|
3078
3508
|
// The remedy depends on what holds the name: another Terminate (end the
|
|
3079
3509
|
// other paths at it) or a step of another kind (rename one).
|
|
3080
3510
|
if (scope.stepNodeId.has(step.name)) {
|
|
@@ -3116,9 +3546,9 @@ export function serialize(built, opts = {}) {
|
|
|
3116
3546
|
const from = tails[0] ?? (router !== undefined ? { node: router, port: step.port } : undefined);
|
|
3117
3547
|
if (!from)
|
|
3118
3548
|
throw new Error(`internal SDK error: .stepToRef('${step.port}', '${step.target}') has no step to leave from.`);
|
|
3119
|
-
refuseRewire('.stepToRef', step.port, lastAction ?? '?', tails);
|
|
3549
|
+
refuseRewire(scope, '.stepToRef', step.port, lastAction ?? '?', tails);
|
|
3120
3550
|
if (step.port === 'error') {
|
|
3121
|
-
refuseErrorEdgeWithoutPort(from.node, lastAction ?? from.node);
|
|
3551
|
+
refuseErrorEdgeWithoutPort(scope, from.node, lastAction ?? from.node);
|
|
3122
3552
|
const node = scope.nodes.find((n) => n.id === from.node);
|
|
3123
3553
|
if (node)
|
|
3124
3554
|
node.inputs = { ...(node.inputs ?? {}), errorHandlingEnabled: true };
|
|
@@ -3166,8 +3596,7 @@ export function serialize(built, opts = {}) {
|
|
|
3166
3596
|
else {
|
|
3167
3597
|
// A repeat that DIFFERS is refused, not renamed `<name>2`: the rename
|
|
3168
3598
|
// turned one End shared by two paths into two Ends with no word said
|
|
3169
|
-
// (#1231)
|
|
3170
|
-
// backstop for `--no-check`.
|
|
3599
|
+
// (#1231): DUP_END_NAME.
|
|
3171
3600
|
if (prior) {
|
|
3172
3601
|
throw new CompileRefusal('DUP_END_NAME', `Two .return({}, { name: '${name}' }) calls ask for the same End with different values, label, version, updates or scope. `
|
|
3173
3602
|
+ `To end several paths at one End, keep one .return and end the others with .stepToRef('${name}'); `
|
|
@@ -3198,10 +3627,11 @@ export function serialize(built, opts = {}) {
|
|
|
3198
3627
|
// id the SDK makes up is the one that moves (#1253).
|
|
3199
3628
|
{
|
|
3200
3629
|
const rootTerminates = new Map();
|
|
3201
|
-
const
|
|
3630
|
+
const rootForks = new Map();
|
|
3631
|
+
const rootSteps = stepNamesOf(built.steps, [], rootTerminates, rootForks);
|
|
3202
3632
|
for (const ep of built.entryPoints ?? [])
|
|
3203
|
-
stepNamesOf(ep.steps, rootSteps, rootTerminates);
|
|
3204
|
-
claimStepIds(root, rootSteps, `flow "${built.id}"`, undefined, [], rootTerminates);
|
|
3633
|
+
stepNamesOf(ep.steps, rootSteps, rootTerminates, rootForks);
|
|
3634
|
+
claimStepIds(root, rootSteps, `flow "${built.id}"`, undefined, [], rootTerminates, rootForks);
|
|
3205
3635
|
const seenEnds = new Set();
|
|
3206
3636
|
const rootNodes = namedNodesOf(built.steps, undefined, seenEnds);
|
|
3207
3637
|
for (const ep of built.entryPoints ?? [])
|
|
@@ -3235,75 +3665,80 @@ export function serialize(built, opts = {}) {
|
|
|
3235
3665
|
throw new CompileRefusal('ENTRY_POINT_DUP_ID', `.entryPoint("${ep.id}"): the id collides with a step, the trigger, or another entry point.`);
|
|
3236
3666
|
}
|
|
3237
3667
|
takenIds.add(ep.id);
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
|
|
3268
|
-
|
|
3269
|
-
|
|
3270
|
-
|
|
3271
|
-
|
|
3272
|
-
|
|
3273
|
-
|
|
3274
|
-
|
|
3275
|
-
|
|
3276
|
-
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3281
|
-
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
3292
|
-
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
|
|
3297
|
-
|
|
3298
|
-
|
|
3299
|
-
|
|
3300
|
-
|
|
3301
|
-
|
|
3302
|
-
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3668
|
+
// Stood in when refused in collect mode, so its prefix and the body are still judged.
|
|
3669
|
+
emitRootNode(ep.id, ep.id, ep.trigger, ep.options?.version, () => {
|
|
3670
|
+
const spec = ep.trigger;
|
|
3671
|
+
const requestedVersion = ep.options?.version;
|
|
3672
|
+
// The author's label; makeNode falls back to the definition's label.
|
|
3673
|
+
const epLabel = ep.options?.label;
|
|
3674
|
+
const customEp = spec instanceof FlowTrigger && !('kind' in spec) ? spec : undefined;
|
|
3675
|
+
const epSpec = spec;
|
|
3676
|
+
let node;
|
|
3677
|
+
if (customEp) {
|
|
3678
|
+
const contribution = contributionOf(customEp, requestedVersion, `entryPoint "${ep.id}"`);
|
|
3679
|
+
const def = resolveContributionDefinition(contribution.definition, requestedVersion, `entryPoint "${ep.id}"`);
|
|
3680
|
+
node = makeNode(def.nodeType, ep.id, ep.options?.label ?? def.display?.label ?? ep.id, { def, requestedVersion });
|
|
3681
|
+
node.inputs = {
|
|
3682
|
+
entryPointId: stableId(`${built.id}:${ep.id}:entryPoint`),
|
|
3683
|
+
...renderDeep(contribution.inputs ?? {}, NO_RENAME),
|
|
3684
|
+
};
|
|
3685
|
+
hoistContributionBindings(def, contribution.bindings, `entryPoint "${ep.id}"`);
|
|
3686
|
+
}
|
|
3687
|
+
else if (epSpec.kind === 'event') {
|
|
3688
|
+
const sub = epSpec.subscription;
|
|
3689
|
+
if (!opts.library)
|
|
3690
|
+
throw libraryRequiredForEvent(opts, sub, ep.id, `onEvent() entry point "${ep.id}"`, 'onEvent()');
|
|
3691
|
+
const rc = resolveEvent(opts.library, 'trigger', sub, 'trigger');
|
|
3692
|
+
refuseUnverifiedByoa(opts, 'trigger', sub);
|
|
3693
|
+
node = makeNode(rc.nodeType, ep.id, epLabel, { def: rc.v1def, requestedVersion });
|
|
3694
|
+
node.inputs = {
|
|
3695
|
+
entryPointId: stableId(`${built.id}:${ep.id}:entryPoint`),
|
|
3696
|
+
detail: eventDetail(rc, sub, ep.id, opts, `onEvent() entry point "${ep.id}"`),
|
|
3697
|
+
};
|
|
3698
|
+
const { entries } = connectionResourceBindings({
|
|
3699
|
+
def: rc.v1def,
|
|
3700
|
+
connectorKey: rc.connectorKey,
|
|
3701
|
+
connectionId: resolveBinding(opts, sub.connection, 'connection', 'onEvent() entry point', eventTarget(sub)),
|
|
3702
|
+
folderKey: resolveBinding(opts, sub.folder, 'folder', 'onEvent() entry point', eventTarget(sub)),
|
|
3703
|
+
}, resourceBindings);
|
|
3704
|
+
for (const b of entries)
|
|
3705
|
+
resourceBindings.set(b.id, b);
|
|
3706
|
+
}
|
|
3707
|
+
else if (epSpec.kind === 'scheduled') {
|
|
3708
|
+
node = makeNode(NODE_TYPE.scheduledTrigger, ep.id, epLabel, {
|
|
3709
|
+
requestedVersion: scheduledVersion(epSpec.inputs, requestedVersion),
|
|
3710
|
+
});
|
|
3711
|
+
node.inputs = {
|
|
3712
|
+
entryPointId: stableId(`${built.id}:${ep.id}:entryPoint`),
|
|
3713
|
+
...scheduleInputs(epSpec.inputs, ep.id),
|
|
3714
|
+
};
|
|
3715
|
+
}
|
|
3716
|
+
else if (epSpec.kind === 'form') {
|
|
3717
|
+
node = makeNode(NODE_TYPE.formTrigger, ep.id, epLabel, { requestedVersion });
|
|
3718
|
+
node.inputs = {
|
|
3719
|
+
entryPointId: stableId(`${built.id}:${ep.id}:entryPoint`),
|
|
3720
|
+
schema: { fields: formTriggerFields(ep.inputs), outcomes: [] },
|
|
3721
|
+
};
|
|
3722
|
+
}
|
|
3723
|
+
else if (epSpec.kind === 'conversation') {
|
|
3724
|
+
node = makeNode(NODE_TYPE.conversationTrigger, ep.id, epLabel, { requestedVersion });
|
|
3725
|
+
node.inputs = { entryPointId: stableId(`${built.id}:${ep.id}:entryPoint`) };
|
|
3726
|
+
}
|
|
3727
|
+
else if (epSpec.kind === 'voice') {
|
|
3728
|
+
node = makeNode(NODE_TYPE.voiceTrigger, ep.id, epLabel, { requestedVersion });
|
|
3729
|
+
node.inputs = { entryPointId: stableId(`${built.id}:${ep.id}:entryPoint`) };
|
|
3730
|
+
}
|
|
3731
|
+
else if (epSpec.kind === 'manual') {
|
|
3732
|
+
node = makeNode(NODE_TYPE.trigger, ep.id, epLabel, { requestedVersion });
|
|
3733
|
+
node.inputs = { entryPointId: stableId(`${built.id}:${ep.id}:entryPoint`) };
|
|
3734
|
+
}
|
|
3735
|
+
else {
|
|
3736
|
+
refuseUnknownTrigger(epSpec, `.entryPoint("${ep.id}")`);
|
|
3737
|
+
throw new Error(`internal SDK error: .entryPoint("${ep.id}"): trigger kind ${JSON.stringify(epSpec.kind)} has no emitter.`);
|
|
3738
|
+
}
|
|
3739
|
+
root.nodes.push(node);
|
|
3740
|
+
return node;
|
|
3741
|
+
});
|
|
3307
3742
|
const prefixTails = emit(ep.steps, [{ node: ep.id, port: 'output' }], root);
|
|
3308
3743
|
for (const t of prefixTails) {
|
|
3309
3744
|
if (!sharedTarget) {
|
|
@@ -3356,7 +3791,7 @@ export function serialize(built, opts = {}) {
|
|
|
3356
3791
|
globals.push({ id: inp.name, name: inp.name, direction: 'in', type: inp.type, triggerNodeId: ep.id, ...varMeta(inp) });
|
|
3357
3792
|
}
|
|
3358
3793
|
}
|
|
3359
|
-
resolveRejoins(root, addEdge);
|
|
3794
|
+
resolveRejoins(root, addEdge, (from, refusal) => refuseOn(root, stepOfNode(root, from.node), refusal));
|
|
3360
3795
|
assertHitlExitInvariant(root.nodes, root.edges, legacyCompletedNodes);
|
|
3361
3796
|
typeAgentInputSchemas(root, globals);
|
|
3362
3797
|
emitStickyNotes(built, root, claimNoteId);
|
|
@@ -3364,10 +3799,10 @@ export function serialize(built, opts = {}) {
|
|
|
3364
3799
|
assertNodeEnvelope(root.nodes, `flow "${built.id}"`);
|
|
3365
3800
|
// The FILE-FORMAT version this document declares: the author's
|
|
3366
3801
|
// `.schemaVersion()` when they set one, else the profile's write floor. It is
|
|
3367
|
-
// resolved HERE
|
|
3368
|
-
//
|
|
3369
|
-
//
|
|
3370
|
-
//
|
|
3802
|
+
// resolved HERE because the serializer must not stamp a version it cannot back
|
|
3803
|
+
// on any path (`compile`, `emitFlow`, a direct `serialize()`); an unwritable
|
|
3804
|
+
// value is SCHEMA_VERSION_UNSUPPORTED, which `check` reports from its
|
|
3805
|
+
// serializer run.
|
|
3371
3806
|
const schemaVersion = resolveSchemaVersion(FLOW_SCHEMA_VERSIONS, built.schemaVersion);
|
|
3372
3807
|
const flow = {
|
|
3373
3808
|
id: built.id,
|