@uipath/maestro-builder-sdk 6.60.0 → 6.61.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +10 -12
  2. package/dist/api-index.md +5 -4
  3. package/dist/check-cli.js +8 -7
  4. package/dist/check.d.ts +11 -0
  5. package/dist/check.js +192 -783
  6. package/dist/cli-library.d.ts +2 -1
  7. package/dist/cli-library.js +7 -2
  8. package/dist/cli-result.d.ts +14 -0
  9. package/dist/cli-result.js +18 -1
  10. package/dist/compile-cli.js +2 -2
  11. package/dist/core/compile-refusal.d.ts +7 -1
  12. package/dist/core/compile-refusal.js +9 -1
  13. package/dist/core/connector-checks.d.ts +8 -30
  14. package/dist/core/connector-checks.js +11 -60
  15. package/dist/core/connector-inputs.js +9 -1
  16. package/dist/core/event-checks.d.ts +15 -2
  17. package/dist/core/event-checks.js +28 -13
  18. package/dist/core/hitl-routing.d.ts +10 -1
  19. package/dist/core/hitl-routing.js +20 -5
  20. package/dist/core/lookups.d.ts +13 -0
  21. package/dist/core/lookups.js +26 -0
  22. package/dist/core/refusals.d.ts +2 -0
  23. package/dist/core/refusals.js +5 -1
  24. package/dist/flow-node-type.d.ts +35 -21
  25. package/dist/flow-parallel-hitl.d.ts +105 -0
  26. package/dist/flow-parallel-hitl.js +214 -0
  27. package/dist/flow-sdk.d.ts +4 -4
  28. package/dist/flow-sdk.js +2 -2
  29. package/dist/flow-terminate-names.d.ts +2 -2
  30. package/dist/flow-terminate-names.js +2 -2
  31. package/dist/merge-compile.js +2 -1
  32. package/dist/nodes/agent/flow/check.js +13 -88
  33. package/dist/nodes/agent/flow/inline-agent.js +41 -14
  34. package/dist/nodes/connector/flow/node-type.js +45 -18
  35. package/dist/nodes/document/flow/ixp.d.ts +1 -1
  36. package/dist/nodes/document/flow/ixp.js +22 -26
  37. package/dist/nodes/hitl/flow/check.js +4 -11
  38. package/dist/nodes/hitl/flow/node-type.js +16 -7
  39. package/dist/nodes/http/flow/check.js +5 -31
  40. package/dist/nodes/http/flow/node-type.js +7 -4
  41. package/dist/nodes/queue/flow/check.d.ts +4 -3
  42. package/dist/nodes/queue/flow/check.js +7 -6
  43. package/dist/nodes/queue/flow/node-type.js +1 -1
  44. package/dist/nodes/transform/flow/check.d.ts +2 -0
  45. package/dist/nodes/transform/flow/check.js +3 -6
  46. package/dist/nodes/transform/flow/node-type.js +6 -6
  47. package/dist/raw-node.js +8 -8
  48. package/dist/serialize.d.ts +38 -3
  49. package/dist/serialize.js +757 -322
  50. package/package.json +2 -2
@@ -1,5 +1,4 @@
1
1
  import { Expr } from '../../../core/expr.js';
2
- import { derivedResourceNodeId } from '../../../core/agent-resource-identity.js';
3
2
  import { GUID_RE } from '../../../core/binding-messages.js';
4
3
  import { prepareCommand } from '../../../core/cli-spelling.js';
5
4
  import { nearestName } from '../../../core/edit-distance.js';
@@ -372,24 +371,7 @@ export function checkInlineAgent(inputs, ctx) {
372
371
  + `run-dependent search needs.`);
373
372
  }
374
373
  }
375
- // The index id is half of the resource node's TYPE
376
- // (`…context.index.<slug>.<id>`), and the platform resolves the index by it.
377
- // A blank, padded or non-GUID id builds a type no tenant serves, and nothing
378
- // downstream says so: the flow compiles, merges and validates, and the agent
379
- // runs ungrounded. So it is an error, not a warning. The id is tested as
380
- // written, not trimmed, because serialize emits it as written.
381
- for (const cx of contexts) {
382
- const id = typeof cx?.id === 'string' ? cx.id : '';
383
- if (!GUID_RE.test(id)) {
384
- at('AGENT_CONTEXT_NO_ID', `Inline agent step "${step}" grounds on context index "${cx?.name ?? ''}" with `
385
- + (id.trim() === '' ? 'no `id`'
386
- : GUID_RE.test(id.trim()) ? `\`id\` ${JSON.stringify(cx?.id)}, which has whitespace around the GUID`
387
- : `\`id\` ${JSON.stringify(cx?.id)}, which is not a GUID`) + '. '
388
- + `The id is the index's key: it is the tail of the resource node's type and how the runtime finds `
389
- + `the index, so without a real one the agent runs ungrounded and \`validate\` does not notice. `
390
- + `\`uip solution resources list --kind Index --source remote --search "<name>"\` returns it as \`Key\`.`);
391
- }
392
- }
374
+ // A context index whose `id` is not a GUID is the serializer's AGENT_CONTEXT_NO_ID.
393
375
  // An index outside the deployment folder needs its folder named. The resource
394
376
  // file carries no `indexId`, so `indexName` + `folderPath` is the whole locator,
395
377
  // and `@uipath/tool-agent`'s `createIndexBinding` leaves the folder off the
@@ -425,67 +407,23 @@ export function checkInlineAgent(inputs, ctx) {
425
407
  }
426
408
  // Where each resource node sits (`nodeId`, `source`): decompile writes them
427
409
  // for a node the canvas placed, and compile writes them as given. A blank or
428
- // repeated id would make compile refuse the flow or overwrite another node,
429
- // and a `source` that is not a uuid names a directory the platform does not
430
- // expect (the agent's own `source` has the same rule).
410
+ // taken `nodeId` and a `source` two of them share are the serializer's
411
+ // INLINE_AGENT_RESOURCE_NODE_ID / INLINE_AGENT_RESOURCE_SOURCE_DUP; a `source`
412
+ // that is not a uuid names a directory the platform does not expect (the
413
+ // agent's own `source` has the same rule).
431
414
  const asList = (v) => (v === undefined ? [] : Array.isArray(v) ? v : [v]);
432
415
  const placed = [
433
- ...asList(inputs?.context).map((r) => ({ what: `context index "${r?.name ?? ''}"`, kind: 'Context', nodeId: r?.nodeId, source: r?.source })),
434
- ...asList(inputs?.tools).map((r) => ({ what: `${r?.kind ?? ''} tool`, kind: 'Tool', nodeId: r?.nodeId, source: r?.source })),
435
- ...asList(inputs?.escalation).map((r) => ({ what: `escalation "${r?.name ?? ''}"`, kind: 'Escalation', nodeId: r?.nodeId, source: r?.source })),
416
+ ...asList(inputs?.context).map((r) => ({ what: `context index "${r?.name ?? ''}"`, source: r?.source })),
417
+ ...asList(inputs?.tools).map((r) => ({ what: `${r?.kind ?? ''} tool`, source: r?.source })),
418
+ ...asList(inputs?.escalation).map((r) => ({ what: `escalation "${r?.name ?? ''}"`, source: r?.source })),
436
419
  ];
437
- // The ids compile derives for the resources with no `nodeId`: the n-th one of
438
- // a kind gets `<step><Kind>`, then `<step><Kind>2`, ... An explicit id equal to
439
- // one of them makes compile refuse the flow, or rename the derived node.
440
- for (const kind of ['Context', 'Tool', 'Escalation']) {
441
- const derived = placed.filter((r) => r.kind === kind && r.nodeId === undefined);
442
- derived.forEach((r, n) => {
443
- const id = derivedResourceNodeId(step, kind, n);
444
- ctx.resources.derivedNodeId(id, `the id compile derives for the ${r.what} of inline agent step "${step}"`);
445
- });
446
- }
447
- /** Explicit `source` -> the resource on THIS agent that has it: two would write one resource.json. */
448
- const sources = new Map();
449
420
  for (const r of placed) {
450
- if (r.nodeId !== undefined) {
451
- const id = typeof r.nodeId === 'string' ? r.nodeId : '';
452
- let other;
453
- if (id.trim() === '') {
454
- at('INLINE_AGENT_RESOURCE_NODE_ID', `Inline agent step "${step}" gives its ${r.what} the \`nodeId\` ${JSON.stringify(r.nodeId)}, which is `
455
- + `not a node id. Omit \`nodeId\` to derive one, or give the id the node has in the flow.`);
456
- }
457
- else if ((other = ctx.resources.claimNodeId(id, r.what)) !== undefined) {
458
- at('INLINE_AGENT_RESOURCE_NODE_ID', `Inline agent step "${step}" gives its ${r.what} the \`nodeId\` "${id}", which the ${other.what} `
459
- + `(on step "${other.step}") already has. Node ids are unique in a flow.`);
460
- }
461
- }
462
- if (r.source === undefined)
463
- continue;
464
421
  // Not trimmed: compile writes `source` as given, into `inputs.source` and the directory name.
465
- if (!GUID_RE.test(String(r.source))) {
422
+ if (r.source !== undefined && !GUID_RE.test(String(r.source))) {
466
423
  at('INLINE_AGENT_RESOURCE_SOURCE_NOT_GUID', `Inline agent step "${step}" gives its ${r.what} the \`source\` ${JSON.stringify(r.source)}, which is not `
467
424
  + `a uuid${/^\s|\s$/.test(String(r.source)) ? ' (it has spaces around it)' : ''}. \`source\` is the resource's `
468
425
  + `directory under the agent's own (\`resources/<source>/resource.json\`) and the node's \`inputs.source\`. `
469
426
  + 'Omit it and one is derived from the flow id, the step and the resource.');
470
- continue;
471
- }
472
- const key = String(r.source).toLowerCase();
473
- const first = sources.get(key);
474
- if (first !== undefined) {
475
- at('INLINE_AGENT_RESOURCE_SOURCE_DUP', `Inline agent step "${step}" gives its ${first} and its ${r.what} the same \`source\` "${String(r.source)}". `
476
- + 'Both would write `resources/<source>/resource.json` under this agent, and the second file would replace '
477
- + 'the first. Give each resource its own `source`, or omit `source` to derive one.');
478
- }
479
- else {
480
- sources.set(key, r.what);
481
- }
482
- }
483
- if (typeof inputs?.source === 'string' && GUID_RE.test(inputs.source.trim())) {
484
- const other = ctx.resources.claimFolder(inputs.source.trim());
485
- if (other !== undefined) {
486
- at('INLINE_AGENT_RESOURCE_SOURCE_DUP', `Inline agent steps "${other}" and "${step}" have the same \`source\` "${inputs.source}". Both would write `
487
- + '`<source>/agent.json`, and the second file would replace the first. Give each agent its own `source`, '
488
- + 'or omit it to derive one.');
489
427
  }
490
428
  }
491
429
  if (inputs?.source !== undefined && !GUID_RE.test(String(inputs.source).trim())) {
@@ -665,17 +603,11 @@ export function checkInlineAgent(inputs, ctx) {
665
603
  // tools happens at compile (where the library is), not here.
666
604
  const toolRefs = inputs?.tools === undefined ? []
667
605
  : Array.isArray(inputs.tools) ? inputs.tools : [inputs.tools];
668
- const BUILTIN_TOOLS = ['analyzefiles', 'summarize', 'batchtransform'];
669
606
  for (const t of toolRefs) {
670
607
  const kind = t?.kind;
671
- if (kind === 'builtin') {
672
- if (!BUILTIN_TOOLS.includes(String(t.tool))) {
673
- at('INLINE_AGENT_TOOL_UNKNOWN_BUILTIN', `Inline agent step "${step}" declares a builtin tool ${JSON.stringify(t.tool)}. `
674
- + `The platform ships exactly three: ${BUILTIN_TOOLS.map((b) => `'${b}'`).join(', ')} — the node type is a `
675
- + `fixed string, so any other name is a node the tenant has never heard of.`, `{ kind: 'builtin', tool: 'summarize' }`);
676
- }
677
- }
678
- else if (kind === 'connector') {
608
+ // A builtin tool other than the three, and a kind no tool has, are the
609
+ // serializer's INLINE_AGENT_TOOL_UNKNOWN_BUILTIN / INLINE_AGENT_TOOL_UNKNOWN_KIND.
610
+ if (kind === 'connector') {
679
611
  const c = t;
680
612
  if (blank(c.connector) || blank(c.operation)) {
681
613
  at('INLINE_AGENT_TOOL_NO_OPERATION', `Inline agent step "${step}" declares a connector tool without ${blank(c.connector) ? 'a `connector`' : 'an `operation`'}. `
@@ -696,9 +628,7 @@ export function checkInlineAgent(inputs, ctx) {
696
628
  + `Name the bindings.json labels the same way \`connector()\` takes them — \`${prepareCommand(key, op)}\` `
697
629
  + `finds the connection and writes both entries.`, `{ kind: 'connector', connector: '${key}', operation: '${op}', connection: '<connection label>', folder: '<folder label>' }`);
698
630
  }
699
- else {
700
- ctx.bindingLabels(`Inline agent step "${step}"'s connector tool`, labels, undefined, blank(c.connector) ? undefined : { key: String(c.connector), ...(blank(c.operation) ? {} : { action: String(c.operation) }) });
701
- }
631
+ // Labels it has are the serializer's to judge against bindings.json (BINDING_*).
702
632
  }
703
633
  else if (kind === 'ixp') {
704
634
  const x = t;
@@ -756,11 +686,6 @@ export function checkInlineAgent(inputs, ctx) {
756
686
  + `tool's contract allows GET, POST, PUT, PATCH, DELETE — anything else the runtime refuses at call time.`);
757
687
  }
758
688
  }
759
- else {
760
- at('INLINE_AGENT_TOOL_UNKNOWN_KIND', `Inline agent step "${step}" declares a tool with kind ${JSON.stringify(kind)}. Valid kinds: 'builtin', `
761
- + `'connector', 'process', 'agent', 'api', 'flow', 'maestro', 'function', 'ixp', 'mcp', 'a2a', `
762
- + `'clientside', 'httpRequest'.`);
763
- }
764
689
  }
765
690
  const escalations = inputs?.escalation === undefined ? []
766
691
  : Array.isArray(inputs.escalation) ? inputs.escalation : [inputs.escalation];
@@ -118,7 +118,7 @@ export function serializeInlineAgent(s, ctx) {
118
118
  source,
119
119
  name: step,
120
120
  });
121
- ctx.sidecar({ path: `${source}/agent.json`, json: agentJson });
121
+ ctx.sidecar({ path: `${source}/agent.json`, json: agentJson }, s.source !== undefined ? `inline agent step "${step}"` : undefined);
122
122
  // The schema's property TYPES need this scope's finished variables, which do
123
123
  // not exist yet: the `properties` object is filled in place when the scope
124
124
  // closes.
@@ -131,12 +131,21 @@ export function serializeInlineAgent(s, ctx) {
131
131
  // is the direction both definitions declare.
132
132
  const contexts = s.context === undefined ? [] : Array.isArray(s.context) ? s.context : [s.context];
133
133
  for (const cx of contexts) {
134
- // `check`'s AGENT_CONTEXT_NO_ID, again for `--no-check`: a blank id builds
135
- // the type `…context.index.<slug>.`, which no tenant serves, and nothing
136
- // after this point would say so.
134
+ // The index id is half of the resource node's TYPE
135
+ // (`…context.index.<slug>.<id>`), and the platform resolves the index by it.
136
+ // A blank, padded or non-GUID id builds a type no tenant serves, and nothing
137
+ // downstream says so: the flow compiles, merges and validates, and the agent
138
+ // runs ungrounded. The id is tested as written, not trimmed, because it is
139
+ // emitted as written.
137
140
  if (typeof cx?.id !== 'string' || !GUID_RE.test(cx.id)) {
138
- throw new CompileRefusal('AGENT_CONTEXT_NO_ID', `inline agent "${step}": context index "${cx?.name ?? ''}" needs its index id (a GUID) as \`id\`; `
139
- + `got ${JSON.stringify(cx?.id)}. \`uip solution resources list --kind Index --source remote --search "<name>"\` returns it as \`Key\`.`);
141
+ const id = typeof cx?.id === 'string' ? cx.id : '';
142
+ throw new CompileRefusal('AGENT_CONTEXT_NO_ID', `Inline agent step "${step}" grounds on context index "${cx?.name ?? ''}" with `
143
+ + (id.trim() === '' ? 'no `id`'
144
+ : GUID_RE.test(id.trim()) ? `\`id\` ${JSON.stringify(cx?.id)}, which has whitespace around the GUID`
145
+ : `\`id\` ${JSON.stringify(cx?.id)}, which is not a GUID`) + '. '
146
+ + `The id is the index's key: it is the tail of the resource node's type and how the runtime finds `
147
+ + `the index, so without a real one the agent runs ungrounded and \`validate\` does not notice. `
148
+ + `\`uip solution resources list --kind Index --source remote --search "<name>"\` returns it as \`Key\`.`);
140
149
  }
141
150
  const cdef = contextIndexDef({ name: cx.name, id: cx.id });
142
151
  // `<step>Context`, deduped for a second index on the same agent, unless the
@@ -188,7 +197,7 @@ export function serializeInlineAgent(s, ctx) {
188
197
  ctx.sidecar({
189
198
  path: `${source}/resources/${contextResourceId}/resource.json`,
190
199
  json: contextResourceJson(cx, { id: contextResourceId, query: contextQuery }),
191
- }, cx.source !== undefined);
200
+ }, cx.source !== undefined ? `inline agent step "${step}"'s context index "${cx.name ?? ''}"` : undefined);
192
201
  }
193
202
  // TOOLS — one resource node per tool, joined to the agent's `tool` handle; the
194
203
  // same artifact shape as context (no incoming control flow, no output, never
@@ -200,6 +209,9 @@ export function serializeInlineAgent(s, ctx) {
200
209
  // — derived, so a recompile is not a diff.
201
210
  const toolRefs = s.tools === undefined ? [] : Array.isArray(s.tools) ? s.tools : [s.tools];
202
211
  for (const tr of toolRefs) {
212
+ // An entry that is no object (null, a hole, a string) has no kind at all.
213
+ if (tr === null || typeof tr !== 'object')
214
+ throw unknownToolKind(step, undefined);
203
215
  let tdef;
204
216
  let label;
205
217
  let extraInputs = {};
@@ -207,8 +219,11 @@ export function serializeInlineAgent(s, ctx) {
207
219
  if (tr.kind === 'builtin') {
208
220
  const bdef = ownEntry(BUILTIN_TOOL_DEFS, tr.tool);
209
221
  tdef = bdef === undefined ? undefined : jsonClone(bdef);
210
- if (!tdef)
211
- throw new CompileRefusal('INLINE_AGENT_TOOL_UNKNOWN_BUILTIN', `inline agent "${step}": unknown builtin tool "${tr.tool}" (analyzefiles | summarize | batchtransform).`);
222
+ if (!tdef) {
223
+ throw new CompileRefusal('INLINE_AGENT_TOOL_UNKNOWN_BUILTIN', `Inline agent step "${step}" declares a builtin tool ${JSON.stringify(tr.tool)}. `
224
+ + `The platform ships exactly three: ${Object.keys(BUILTIN_TOOL_DEFS).map((b) => `'${b}'`).join(', ')} — the node `
225
+ + `type is a fixed string, so any other name is a node the tenant has never heard of.`, `{ kind: 'builtin', tool: 'summarize' }`);
226
+ }
212
227
  label = tdef.display.label;
213
228
  // The designer-placed node carries the def's `inputDefaults` with the
214
229
  // author's values over them — the context rule, kept per builtin (each def
@@ -242,8 +257,14 @@ export function serializeInlineAgent(s, ctx) {
242
257
  extraInputs = jsonClone(tdef.inputDefaults);
243
258
  }
244
259
  else if (tr.kind === 'connector') {
245
- if (!ctx.library)
260
+ if (!ctx.library) {
261
+ // Its labels are bindings.json's to judge, as a connector step's are:
262
+ // refused ahead of the library, so a run without one still hears about them.
263
+ const where = `inline agent "${step}"'s connector tool "${tr.connector}/${tr.operation}"`;
264
+ ctx.resolveBinding(tr.connection, 'connection', where, { key: tr.connector, action: tr.operation });
265
+ ctx.resolveBinding(tr.folder, 'folder', where, { key: tr.connector, action: tr.operation });
246
266
  throw new CompileRefusal('LIBRARY_REQUIRED', `inline agent "${step}"'s connector tool needs a library (pass --library / set $FLOW_SDK_LIBRARY_JSON).`);
267
+ }
247
268
  const rc = ctx.library.resolve(tr.connector, tr.operation, tr.version, tr.object);
248
269
  // An entry `prepare` synthesized from Integration Service compiles in BPMN
249
270
  // only — for a tool as for a step: `connectorToolDef` reads the same
@@ -338,7 +359,7 @@ export function serializeInlineAgent(s, ctx) {
338
359
  else {
339
360
  const meta = ownEntry(PROCESS_TOOL_META, tr.kind);
340
361
  if (!meta)
341
- throw new CompileRefusal('INLINE_AGENT_TOOL_UNKNOWN_KIND', `inline agent "${step}": unknown tool kind "${tr.kind}".`);
362
+ throw unknownToolKind(step, tr.kind);
342
363
  tdef = processToolDef(tr.kind, tr);
343
364
  label = tr.name;
344
365
  // The definition's `model.context` names `<bindings.name>` /
@@ -377,7 +398,7 @@ export function serializeInlineAgent(s, ctx) {
377
398
  ? httpRequestToolResourceJson(label, toolInputs, { id: toolResourceId, canvasNodeId: tid })
378
399
  : toolResourceJson(tr, { id: toolResourceId });
379
400
  if (toolResource !== undefined) {
380
- ctx.sidecar({ path: `${source}/resources/${toolResourceId}/resource.json`, json: toolResource }, tr.source !== undefined);
401
+ ctx.sidecar({ path: `${source}/resources/${toolResourceId}/resource.json`, json: toolResource }, tr.source !== undefined ? `inline agent step "${step}"'s ${tr.kind} tool` : undefined);
381
402
  }
382
403
  }
383
404
  // ESCALATION — the agent's TOP handle; an Action Center app task a human
@@ -443,7 +464,7 @@ export function serializeInlineAgent(s, ctx) {
443
464
  ctx.sidecar({
444
465
  path: `${source}/resources/${qsource}/resource.json`,
445
466
  json: escalationResourceJson(esc, { id: qsource, channelId, quickFormSchema: schema }),
446
- }, esc.source !== undefined);
467
+ }, esc.source !== undefined ? `inline agent step "${step}"'s escalation "${esc.name ?? ''}"` : undefined);
447
468
  continue;
448
469
  }
449
470
  const app = esc.app;
@@ -486,7 +507,7 @@ export function serializeInlineAgent(s, ctx) {
486
507
  ctx.sidecar({
487
508
  path: `${source}/resources/${esource}/resource.json`,
488
509
  json: escalationResourceJson(esc, { id: esource, channelId }),
489
- }, esc.source !== undefined);
510
+ }, esc.source !== undefined ? `inline agent step "${step}"'s escalation "${esc.name ?? ''}"` : undefined);
490
511
  }
491
512
  // MEMORY — one episodic-memory resource on the agent's `memory` handle (the
492
513
  // 1.4 definition selected above). Same artifact shape as context.
@@ -525,3 +546,9 @@ export function serializeInlineAgent(s, ctx) {
525
546
  attach,
526
547
  };
527
548
  }
549
+ /** INLINE_AGENT_TOOL_UNKNOWN_KIND: a tool whose `kind` names no tool the platform has. */
550
+ function unknownToolKind(step, kind) {
551
+ return new CompileRefusal('INLINE_AGENT_TOOL_UNKNOWN_KIND', `Inline agent step "${step}" declares a tool with kind ${JSON.stringify(kind)}. Valid `
552
+ + "kinds: 'builtin', 'connector', 'process', 'agent', 'api', 'flow', 'maestro', 'function', 'ixp', 'mcp', 'a2a', "
553
+ + "'clientside', 'httpRequest'.");
554
+ }
@@ -65,15 +65,36 @@ function listOutputs(rc, nodeId) {
65
65
  }
66
66
  function serializeConnector(spec, ctx) {
67
67
  const step = ctx.step;
68
- if (!ctx.library)
68
+ if (!ctx.library) {
69
+ // The labels are bindings.json's to judge, not the library's: refused ahead
70
+ // of the library, so a run without one (`check`'s inner loop) still hears
71
+ // about them. With a library they are judged below, as they always were.
72
+ const where = `connector "${step}"`;
73
+ const target = { key: spec.key, action: spec.action };
74
+ const missing = missingBindingLabels(spec);
75
+ if (missing.length > 0 && !ctx.drafting)
76
+ throw new BindingRefusal('BINDING_MISSING', bindingMissingMessage(where, missing, target));
77
+ ctx.resolveBinding(spec.connection, 'connection', where, target);
78
+ ctx.resolveBinding(spec.folder, 'folder', where, target);
69
79
  throw new CompileRefusal('LIBRARY_REQUIRED', `connector "${step}" needs a library (pass --library / set $FLOW_SDK_LIBRARY_JSON).`);
80
+ }
81
+ // The library's refusals (the operation, its fields) name the connector, not
82
+ // the step: they say which step as `check` always has.
83
+ const onStep = (refusal) => new CompileRefusal(refusal.code, `Connector step "${step}": ${refusal.message}`, refusal.suggestion);
70
84
  let rc;
71
85
  try {
72
86
  rc = ctx.library.resolve(spec.key, spec.action, spec.version, spec.object);
73
87
  }
74
88
  catch (error) {
75
- if (!ctx.drafting || !(error instanceof ConnectorResolveError) || error.code !== 'CONNECTOR_OBJECT_REQUIRED')
89
+ if (error instanceof Error && error.message.startsWith('internal SDK error:'))
76
90
  throw error;
91
+ // A throw `library.resolve` makes on no purpose is still this step's refusal,
92
+ // under the code `check` has always given it (#1015).
93
+ if (!(error instanceof ConnectorResolveError)) {
94
+ throw onStep(new CompileRefusal('CONNECTOR_RESOLVE_FAILED', error instanceof Error ? error.message : String(error)));
95
+ }
96
+ if (!ctx.drafting || error.code !== 'CONNECTOR_OBJECT_REQUIRED')
97
+ throw onStep(error);
77
98
  // A generic operation saved before its object was picked has no definition
78
99
  // to emit against. Draft tolerance emits a stable stand-in under the node's
79
100
  // id; merge restores the original when it is untouched and refuses it
@@ -100,11 +121,10 @@ function serializeConnector(spec, ctx) {
100
121
  // An entry `prepare` synthesized from Integration Service compiles in BPMN only.
101
122
  const noDefinition = synthesizedDefinitionRefusal(rc);
102
123
  if (noDefinition)
103
- throw noDefinition;
124
+ throw onStep(noDefinition);
104
125
  const where = `connector "${step}"`;
105
126
  const target = { key: spec.key, action: spec.action };
106
- // A connector-bound node needs both labels (#890); check says so first
107
- // (BINDING_MISSING). A draft that names no connection gets no binding rows
127
+ // A connector-bound node needs both labels (#890: BINDING_MISSING). A draft that names no connection gets no binding rows
108
128
  // either: there is nothing to bind, and rows with an empty resourceKey would
109
129
  // leak into the merged file through merge's bindings union.
110
130
  let unbound = false;
@@ -129,19 +149,26 @@ function serializeConnector(spec, ctx) {
129
149
  });
130
150
  }
131
151
  const library = ctx.library;
132
- const connectorInputs = buildConnectorInputs(rc, spec.inputs, {
133
- ...(ctx.drafting
134
- ? { onMissingRequired: (m) => ctx.draft(CONNECTOR_INPUT_MISSING, `connector ${rc.nodeType}: ${m}`) }
135
- : {}),
136
- dialect: FLOW_EXPR_DIALECT,
137
- render: (value) => ctx.renderDeep(value),
138
- resolutions: library.lookupResolutions ?? {},
139
- // Only for the missing-required hint; a test double that is not a full
140
- // Library simply gets the plain message.
141
- lookups: typeof library.lookupSpecs === 'function'
142
- ? library.lookupSpecs(spec.key, spec.action, spec.version, spec.object)
143
- : {},
144
- });
152
+ const connectorInputs = (() => {
153
+ try {
154
+ return buildConnectorInputs(rc, spec.inputs, {
155
+ ...(ctx.drafting
156
+ ? { onMissingRequired: (m) => ctx.draft(CONNECTOR_INPUT_MISSING, `connector ${rc.nodeType}: ${m}`) }
157
+ : {}),
158
+ dialect: FLOW_EXPR_DIALECT,
159
+ render: (value) => ctx.renderDeep(value),
160
+ resolutions: library.lookupResolutions ?? {},
161
+ // Only for the missing-required hint; a test double that is not a full
162
+ // Library simply gets the plain message.
163
+ lookups: typeof library.lookupSpecs === 'function'
164
+ ? library.lookupSpecs(spec.key, spec.action, spec.version, spec.object)
165
+ : {},
166
+ });
167
+ }
168
+ catch (error) {
169
+ throw error instanceof CompileRefusal ? onStep(error) : error;
170
+ }
171
+ })();
145
172
  const replayInputs = replayInputsOf(connectorInputs);
146
173
  const detail = {
147
174
  connector: rc.connectorKey,
@@ -30,5 +30,5 @@ export declare function ixpExtractDef(spec: {
30
30
  folderPath?: string;
31
31
  versionTag?: string;
32
32
  description?: string;
33
- }): any;
33
+ }, step?: string): any;
34
34
  export declare const ixpExtractNodeType: FlowVersionedNodeType<IxpExtractSpec>;
@@ -63,15 +63,25 @@ const IXP_DEF = ixpDefinition;
63
63
  * validate` and Studio Web's property panel refuse that). So the emitter writes
64
64
  * the defaults and the node reads them back, rather than the two drifting.
65
65
  */
66
- export function ixpExtractDef(spec) {
67
- const parts = parseIxpProjectType(spec.project);
66
+ export function ixpExtractDef(spec, step = 'ixpExtract') {
67
+ const parts = typeof spec.project === 'string' ? parseIxpProjectType(spec.project) : undefined;
68
68
  if (!parts) {
69
- // The three conditions check tells apart, under check's three codes.
70
- const code = typeof spec.project !== 'string' || spec.project.trim() === '' ? 'IXP_NO_PROJECT'
71
- : !spec.project.startsWith('uipath.ixp.') ? 'IXP_PROJECT_NOT_NODE_TYPE' : 'IXP_PROJECT_TAIL_UNPARSEABLE';
72
- throw new CompileRefusal(code, `ixpExtract: "${spec.project}" is not a resolvable extractor node type. It must be `
73
- + `uipath.ixp.<slug>.<projectVersionId>-<folderKey> — copy it from `
74
- + `\`uip maestro flow registry pull --force && uip maestro flow registry search -f "type:startsWith=uipath.ixp"\`.`);
69
+ // Three conditions, three codes, each with its remedy.
70
+ const project = typeof spec.project === 'string' ? spec.project.trim() : '';
71
+ if (project === '') {
72
+ throw new CompileRefusal('IXP_NO_PROJECT', `IxP step "${step}" has no \`project\`. That is the extractor's whole node type, and it carries the `
73
+ + `project version id and folder key the platform resolves the deployment by. Copy it from the registry `
74
+ + `— never construct it: three different spellings of its tail exist in deployed flows.`, `project: 'uipath.ixp.<slug>.<projectVersionId>-<folderKey>' // uip maestro flow registry pull --force && uip maestro flow registry search -f "type:startsWith=uipath.ixp"`);
75
+ }
76
+ if (!project.startsWith('uipath.ixp.')) {
77
+ throw new CompileRefusal('IXP_PROJECT_NOT_NODE_TYPE', `IxP step "${step}"'s \`project\` is ${JSON.stringify(spec.project)}, which is not a \`uipath.ixp.*\` node `
78
+ + `type. This field takes the type verbatim from \`registry search\`, not the project's name or id — the `
79
+ + `name goes in \`name\` and the slug in \`modelName\`.`);
80
+ }
81
+ throw new CompileRefusal('IXP_PROJECT_TAIL_UNPARSEABLE', `IxP step "${step}"'s \`project\` ends in ${JSON.stringify(project.slice(project.lastIndexOf('.') + 1))}, which is `
82
+ + `not the \`<projectVersionId>-<folderKey>\` pair of uuids the current registry serves. Two older spellings `
83
+ + `exist in the corpus — a sanitized folder path, and the literal string "undefined" — and neither resolves `
84
+ + `today. Re-read the type from \`registry search\` after \`registry pull --force\`.`);
75
85
  }
76
86
  const { versionId, folderKey } = parts;
77
87
  const model = {
@@ -223,23 +233,9 @@ function checkIxpExtract(inputs, ctx) {
223
233
  const step = ctx.step;
224
234
  const at = (code, message, suggestion) => ctx.report(code, message, suggestion);
225
235
  const blank = (v) => v === undefined || v === null || (typeof v === 'string' && v.trim() === '');
226
- const project = String(inputs?.project ?? '').trim();
227
- if (blank(project)) {
228
- at('IXP_NO_PROJECT', `IxP step "${step}" has no \`project\`. That is the extractor's whole node type, and it carries the `
229
- + `project version id and folder key the platform resolves the deployment by. Copy it from the registry `
230
- + `— never construct it: three different spellings of its tail exist in deployed flows.`, `project: 'uipath.ixp.<slug>.<projectVersionId>-<folderKey>' // uip maestro flow registry pull --force && uip maestro flow registry search -f "type:startsWith=uipath.ixp"`);
231
- }
232
- else if (!project.startsWith('uipath.ixp.')) {
233
- at('IXP_PROJECT_NOT_NODE_TYPE', `IxP step "${step}"'s \`project\` is ${JSON.stringify(inputs.project)}, which is not a \`uipath.ixp.*\` node `
234
- + `type. This field takes the type verbatim from \`registry search\`, not the project's name or id — the `
235
- + `name goes in \`name\` and the slug in \`modelName\`.`);
236
- }
237
- else if (parseIxpProjectType(project) === undefined) {
238
- at('IXP_PROJECT_TAIL_UNPARSEABLE', `IxP step "${step}"'s \`project\` ends in ${JSON.stringify(project.slice(project.lastIndexOf('.') + 1))}, which is `
239
- + `not the \`<projectVersionId>-<folderKey>\` pair of uuids the current registry serves. Two older spellings `
240
- + `exist in the corpus — a sanitized folder path, and the literal string "undefined" — and neither resolves `
241
- + `today. Re-read the type from \`registry search\` after \`registry pull --force\`.`);
242
- }
236
+ // A `project` that is missing, not a `uipath.ixp.*` type, or whose tail does
237
+ // not parse is the serializer's IXP_NO_PROJECT / IXP_PROJECT_NOT_NODE_TYPE /
238
+ // IXP_PROJECT_TAIL_UNPARSEABLE.
243
239
  if (blank(inputs?.modelName)) {
244
240
  at('IXP_NO_MODEL_NAME', `IxP step "${step}" has no \`modelName\`. The definition marks it required (\`minLength: 1\`) and the `
245
241
  + `platform's \`ixp-node\` validator refuses an empty one — it is the project's own name as the tenant `
@@ -273,7 +269,7 @@ function checkIxpExtract(inputs, ctx) {
273
269
  * author named, and the fixed output pair.
274
270
  */
275
271
  function serializeIxpExtract(s, ctx) {
276
- const def = ixpExtractDef(s); // throws on an unparseable project
272
+ const def = ixpExtractDef(s, ctx.step); // throws on an unparseable project
277
273
  const { versionId } = parseIxpProjectType(s.project); // ixpExtractDef threw if it could not parse
278
274
  // The bindings first: two steps on the same project share ONE pair, and the
279
275
  // definition's `model.bindings.resourceKey` names the key they hang off.
@@ -88,15 +88,10 @@ export function checkHitl(inputs, ctx) {
88
88
  // Per-outcome exits are not a version question: quick-form draws one handle
89
89
  // per outcome like the base node, so it may ask for them; the two app-backed
90
90
  // variants read their outcomes from the app and leave on `completed`.
91
+ // Asking a variant for either is the serializer's HITL_OUTCOME_PORTS_VARIANT;
92
+ // the rules below assume a task that can have what it asks for.
91
93
  if (variant !== undefined
92
94
  && (inputs?.exposeError === true || (inputs?.outcomePorts === true && !hitlDerivesOutcomeHandles(variant)))) {
93
- at('HITL_OUTCOME_PORTS_VARIANT', inputs?.exposeError === true
94
- ? `Human task "${step}" asks for exposeError, but its variant is "${variant}" — the error output `
95
- + `exists only on the base node type's 1.2 definition.`
96
- : `Human task "${step}" asks for outcomePorts, but its variant is "${variant}", which reads its `
97
- + `outcomes from its app and leaves on its single 'completed' exit.`, inputs?.exposeError === true
98
- ? 'Omit `variant`, or drop exposeError.'
99
- : `Route on out('${step}', 'Action') downstream.`);
100
95
  return;
101
96
  }
102
97
  // `legacyCompleted: true` keeps a pre-#3512 file's `completed` edge (#1197).
@@ -104,11 +99,9 @@ export function checkHitl(inputs, ctx) {
104
99
  // rarely what the author wants, and `flow validate` says nothing about it.
105
100
  if (inputs?.legacyCompleted === true) {
106
101
  const outcomes = Array.isArray(inputs?.outcomes) ? inputs.outcomes : [];
107
- if (inputs?.outcomePorts !== undefined || inputs?.exposeError === true) {
108
- at('HITL_LEGACY_COMPLETED_CONFLICT', `Human task "${step}" sets legacyCompleted: true, which leaves it on its single 'completed' handle, and also `
109
- + `${inputs?.exposeError === true ? 'exposeError' : 'outcomePorts'}, which asks for per-outcome exits.`, 'Drop `legacyCompleted`.');
102
+ // With outcomePorts or exposeError too it is the serializer's HITL_LEGACY_COMPLETED_CONFLICT.
103
+ if (inputs?.outcomePorts !== undefined || inputs?.exposeError === true)
110
104
  return;
111
- }
112
105
  if (!hitlDerivesOutcomeHandles(variant) || outcomes.length === 0) {
113
106
  // Its own code: this flow compiles, so the code is check's alone.
114
107
  at('HITL_LEGACY_COMPLETED_NO_EFFECT', `Human task "${step}" sets legacyCompleted: true, but ${outcomes.length === 0 ? 'it has no outcomes'
@@ -579,16 +579,16 @@ function hitlAt(typeName, version, factory) {
579
579
  const pinned = ctx.pinnedVersion;
580
580
  const exitModel = hitlExitModel(inputs, pinned);
581
581
  if (typeName !== BASE && inputs?.exposeError === true) {
582
- throw new CompileRefusal('HITL_OUTCOME_PORTS_VARIANT', `"${ctx.step}": exposeError needs the base human task's 1.2 definition, which adds the `
583
- + `error output — the '${inputs?.variant}' variant has no such version. Drop the variant or exposeError.`);
582
+ throw new CompileRefusal('HITL_OUTCOME_PORTS_VARIANT', `Human task "${ctx.step}" asks for exposeError, but its variant is "${inputs?.variant}" — the error output `
583
+ + `exists only on the base node type's 1.2 definition.`, 'Omit `variant`, or drop exposeError.');
584
584
  }
585
585
  if (inputs?.outcomePorts === true && !OUTCOME_HANDLE_NODE_TYPES.has(typeName)) {
586
- throw new CompileRefusal('HITL_OUTCOME_PORTS_VARIANT', `"${ctx.step}": outcomePorts: true, but the '${inputs?.variant}' variant reads its outcomes `
587
- + `from its app and leaves on its single 'completed' exit. Route on out('${ctx.step}', 'Action') downstream.`);
586
+ throw new CompileRefusal('HITL_OUTCOME_PORTS_VARIANT', `Human task "${ctx.step}" asks for outcomePorts, but its variant is "${inputs?.variant}", which reads its `
587
+ + `outcomes from its app and leaves on its single 'completed' exit.`, `Route on out('${ctx.step}', 'Action') downstream.`);
588
588
  }
589
589
  if (inputs?.legacyCompleted === true && (inputs?.outcomePorts !== undefined || inputs?.exposeError === true)) {
590
- throw new CompileRefusal('HITL_LEGACY_COMPLETED_CONFLICT', `"${ctx.step}": legacyCompleted: true leaves the task on its single 'completed' handle, so it cannot also `
591
- + `${inputs?.exposeError === true ? 'expose the error output (exposeError)' : 'set outcomePorts'}. Drop one.`);
590
+ throw new CompileRefusal('HITL_LEGACY_COMPLETED_CONFLICT', `Human task "${ctx.step}" sets legacyCompleted: true, which leaves it on its single 'completed' handle, and also `
591
+ + `${inputs?.exposeError === true ? 'exposeError' : 'outcomePorts'}, which asks for per-outcome exits.`, 'Drop `legacyCompleted`.');
592
592
  }
593
593
  if (inputs?.legacyCompleted === true && typeName === BASE && (pinned === '1.1' || pinned === '1.2')) {
594
594
  throw new CompileRefusal('HITL_LEGACY_COMPLETED_VERSION', `"${ctx.step}": legacyCompleted: true, but { version: '${pinned}' } pins the task's per-outcome definition, `
@@ -615,7 +615,16 @@ function hitlAt(typeName, version, factory) {
615
615
  // Every human-task definition declares one static source handle,
616
616
  // `completed`: the real exit on an app-backed task and on one with no
617
617
  // outcomes, and the author's explicit choice with `legacyCompleted: true`.
618
- return { ...contribution, port: 'completed', ...(exitModel.legacyCompleted ? { legacyCompleted: true } : {}) };
618
+ const outcomes = Array.isArray(inputs?.outcomes) ? inputs.outcomes : [];
619
+ const noOutcomePorts = {
620
+ why: outcomes.length === 0 ? 'it declares no outcomes'
621
+ : exitModel.legacyCompleted ? 'it sets legacyCompleted: true'
622
+ : `the '${String(inputs?.variant)}' variant reads its outcomes from its app`,
623
+ suggestion: exitModel.legacyCompleted
624
+ ? 'drop `legacyCompleted` (and set outcomePorts: true to route each outcome)'
625
+ : `route on out('${ctx.step}', 'Action') downstream`,
626
+ };
627
+ return { ...contribution, port: 'completed', noOutcomePorts, ...(exitModel.legacyCompleted ? { legacyCompleted: true } : {}) };
619
628
  },
620
629
  deserialize: (node, ctx) => liftHitl(node, typeName, ctx),
621
630
  print: printHitl,
@@ -88,27 +88,9 @@ export function checkHttp(inputs, ctx) {
88
88
  + `Integration Service connection or an .onError() handler.`);
89
89
  }
90
90
  }
91
- const connection = inputs?.connection;
92
- const folder = inputs?.folder;
93
- const targetConnector = inputs?.targetConnector;
94
- const bindingConfigured = connection !== undefined || folder !== undefined;
95
- const targetConfigured = targetConnector !== undefined;
96
- const nonEmpty = (value) => typeof value === 'string' && value.trim().length > 0;
97
- if (!inputs?.managed && (bindingConfigured || targetConfigured)) {
98
- ctx.report('HTTP_BINDING_REQUIRES_MANAGED', `Step "${step}" supplies Integration Service connection settings to the standalone HTTP node. `
99
- + `Only the managed node can use connection, folder, or targetConnector.`, `managed: true`);
100
- }
101
- else if (inputs?.managed && bindingConfigured && (!nonEmpty(connection) || !nonEmpty(folder))) {
102
- ctx.report('HTTP_MANAGED_BINDING_INCOMPLETE', `Managed HTTP step "${step}" must name both a non-empty \`connection\` and \`folder\`. The platform `
103
- + `needs the pair to resolve the Integration Service connection at runtime.`, `connection: 'http', folder: 'shared'`);
104
- }
105
- if (inputs?.managed && targetConfigured && !nonEmpty(targetConnector)) {
106
- ctx.report('HTTP_TARGET_CONNECTOR_EMPTY', `Managed HTTP step "${step}" sets an empty \`targetConnector\`; name its Integration Service connector key.`, `targetConnector: 'uipath-salesforce-slack'`);
107
- }
108
- else if (inputs?.managed && targetConfigured && !bindingConfigured) {
109
- ctx.report('HTTP_TARGET_CONNECTOR_REQUIRES_BINDING', `Managed HTTP step "${step}" sets \`targetConnector\` without a connection and folder. `
110
- + `A connector-specific fallback needs the binding pair whose authentication it reuses.`, `connection: 'slack', folder: 'shared'`);
111
- }
91
+ // Connection settings on the standalone node, an incomplete pair, and an
92
+ // empty or unbound `targetConnector` are the serializer's HTTP_BINDING_* /
93
+ // HTTP_MANAGED_BINDING_INCOMPLETE / HTTP_TARGET_CONNECTOR_*.
112
94
  const timeout = inputs?.timeout;
113
95
  if (timeout !== undefined) {
114
96
  if (typeof timeout !== 'string' || !ISO8601_DURATION.test(timeout.trim())) {
@@ -138,16 +120,8 @@ export function checkHttp(inputs, ctx) {
138
120
  export function checkHttpStep(inputs, ctx) {
139
121
  const step = ctx.step;
140
122
  checkHttp(inputs, ctx);
141
- // Only a complete pair: an empty half is HTTP_MANAGED_BINDING_INCOMPLETE, which
142
- // serialize refuses before it resolves either name.
143
- const pair = inputs;
144
- const filled = (v) => typeof v === 'string' && v.trim() !== '';
145
- if (pair?.managed && filled(pair.connection) && filled(pair.folder)) {
146
- const target = pair.targetConnector;
147
- ctx.bindingLabels(`managed http "${step}"`, pair,
148
- // The connector its CONNECTION belongs to: the target, not the wrapper.
149
- { id: step, connector: typeof target === 'string' && target !== '' ? target : 'uipath-uipath-http' });
150
- }
123
+ // A managed step's binding labels are the serializer's (BINDING_*), as a
124
+ // connector step's are.
151
125
  const seen = new Set();
152
126
  for (const b of inputs?.branches ?? []) {
153
127
  if (!/^[A-Za-z0-9_-]+$/.test(b.name)) {