@uipath/maestro-builder-sdk 6.23.0 → 6.25.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 (46) hide show
  1. package/dist/api-index.md +121 -121
  2. package/dist/api-members.md +362 -362
  3. package/dist/check.js +1 -444
  4. package/dist/core/actions.d.ts +0 -404
  5. package/dist/core/actions.js +0 -111
  6. package/dist/core/form-field-type.d.ts +16 -0
  7. package/dist/core/form-field-type.js +33 -0
  8. package/dist/core/hitl-routing.d.ts +17 -3
  9. package/dist/core/stable-id.d.ts +9 -0
  10. package/dist/core/stable-id.js +16 -0
  11. package/dist/core/step-ports.d.ts +25 -1
  12. package/dist/core/step-ports.js +6 -47
  13. package/dist/core/typed-raw-node.js +0 -11
  14. package/dist/decompile.js +7 -235
  15. package/dist/flow-node-registry.js +3 -1
  16. package/dist/flow-node-type.d.ts +45 -1
  17. package/dist/flow-sdk.d.ts +2 -0
  18. package/dist/flow-sdk.js +2 -0
  19. package/dist/flow-step-exits.js +1 -1
  20. package/dist/nodes/conversation/flow/check.d.ts +17 -0
  21. package/dist/nodes/conversation/flow/check.js +54 -0
  22. package/dist/nodes/conversation/flow/factories.d.ts +58 -0
  23. package/dist/nodes/conversation/flow/factories.js +63 -0
  24. package/dist/nodes/conversation/flow/family.d.ts +3 -0
  25. package/dist/nodes/conversation/flow/family.js +8 -0
  26. package/dist/nodes/conversation/flow/index.d.ts +3 -0
  27. package/dist/nodes/conversation/flow/index.js +3 -0
  28. package/dist/nodes/conversation/flow/node-type.d.ts +14 -0
  29. package/dist/nodes/conversation/flow/node-type.js +117 -0
  30. package/dist/nodes/conversation/flow/spec.d.ts +60 -0
  31. package/dist/nodes/conversation/flow/spec.js +1 -0
  32. package/dist/nodes/hitl/flow/check.d.ts +38 -0
  33. package/dist/nodes/hitl/flow/check.js +357 -0
  34. package/dist/nodes/hitl/flow/factories.d.ts +58 -0
  35. package/dist/nodes/hitl/flow/factories.js +59 -0
  36. package/dist/nodes/hitl/flow/family.d.ts +3 -0
  37. package/dist/nodes/hitl/flow/family.js +8 -0
  38. package/dist/nodes/hitl/flow/index.d.ts +3 -0
  39. package/dist/nodes/hitl/flow/index.js +3 -0
  40. package/dist/nodes/hitl/flow/node-type.d.ts +7 -0
  41. package/dist/nodes/hitl/flow/node-type.js +601 -0
  42. package/dist/nodes/hitl/flow/spec.d.ts +265 -0
  43. package/dist/nodes/hitl/flow/spec.js +1 -0
  44. package/dist/serialize.d.ts +2 -4
  45. package/dist/serialize.js +35 -392
  46. package/package.json +2 -2
@@ -91,3 +91,19 @@ export function sha256Uuid(seed) {
91
91
  const h = createHash('sha256').update(seed).digest('hex');
92
92
  return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
93
93
  }
94
+ /**
95
+ * A deterministic UUID for the form schema, derived from the flow and node ids.
96
+ *
97
+ * The platform uses `schemaId` to identify and cache a form, so it has to be
98
+ * stable across builds of the same flow — a random one would make every rebuild a
99
+ * different form and every emitted `.flow` a different file. Shaped as a v5 UUID
100
+ * (SHA-1 based, version + variant bits set) because that is what the field holds.
101
+ */
102
+ export function schemaId(flowId, nodeId) {
103
+ const h = createHash('sha1').update(`uipath.human-in-the-loop:${flowId}:${nodeId}`).digest();
104
+ const b = Buffer.from(h.subarray(0, 16));
105
+ b[6] = (b[6] & 0x0f) | 0x50;
106
+ b[8] = (b[8] & 0x3f) | 0x80;
107
+ const hex = b.toString('hex');
108
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
109
+ }
@@ -1,3 +1,27 @@
1
+ /**
2
+ * The exits an action declares that the AUTHOR named — a human task's outcomes,
3
+ * an HTTP call's response branches — and the port each one leaves on. Every
4
+ * family that has them is a registered node type and declares its own
5
+ * (`FlowNodeType.exits`); `stepExits` in flow-step-exits.ts asks it first, and
6
+ * what is left here is the reason a kind has none.
7
+ *
8
+ * This is what `.stepSwitch()` routes over, and the reason it exists as a table
9
+ * rather than as more `if (spec.kind === …)` arms: `serialize` has to turn
10
+ * an arm's `value` into a port, `check` has to say which values are legal and
11
+ * which were left out, and `decompile` has to turn a port back into a value.
12
+ * Those three read the same list instead of each carrying their own copy of
13
+ * "how a human task names its ports".
14
+ *
15
+ * `undefined` means the family has no author-declared fan-out at all, which is
16
+ * what makes `.stepSwitch()` on it an error rather than a no-op.
17
+ *
18
+ * The `error` port is deliberately NOT here. It is declared by the definition
19
+ * rather than by the author, it exists on families that have no other fan-out,
20
+ * and it is already routed by `.onError()` / `.stepToList('error', …)`. Folding
21
+ * it in would make every action a `.stepSwitch` candidate and would put failure
22
+ * handling and outcome routing in one list, where an omitted arm would mean two
23
+ * different things.
24
+ */
1
25
  /** One author-named exit: the value an arm matches on, and the port it wires. */
2
26
  export interface DeclaredExit {
3
27
  /** What `.stepSwitch()`'s arm `value` matches — the outcome or branch NAME. */
@@ -31,7 +55,7 @@ type SpecLike = {
31
55
  * The author-declared exits of `spec`, or a `NoExits` explaining why there are
32
56
  * none to route.
33
57
  */
34
- export declare function declaredExits(spec: SpecLike | undefined, pinnedVersion?: string): DeclaredExit[] | NoExits;
58
+ export declare function declaredExits(spec: SpecLike | undefined, _pinnedVersion?: string): DeclaredExit[] | NoExits;
35
59
  /** Narrowing helper — `declaredExits` returns one or the other. */
36
60
  export declare function hasExits(r: DeclaredExit[] | NoExits): r is DeclaredExit[];
37
61
  export {};
@@ -1,8 +1,9 @@
1
1
  /**
2
- * The exits an action declares that the AUTHOR named — a human task's outcomes —
3
- * and the port each one leaves on. A registered node type declares its own
4
- * (`FlowNodeType.exits`: an HTTP call's response branches); `stepExits` in
5
- * flow-step-exits.ts asks it first and falls back here.
2
+ * The exits an action declares that the AUTHOR named — a human task's outcomes,
3
+ * an HTTP call's response branches — and the port each one leaves on. Every
4
+ * family that has them is a registered node type and declares its own
5
+ * (`FlowNodeType.exits`); `stepExits` in flow-step-exits.ts asks it first, and
6
+ * what is left here is the reason a kind has none.
6
7
  *
7
8
  * This is what `.stepSwitch()` routes over, and the reason it exists as a table
8
9
  * rather than as more `if (spec.kind === …)` arms: `serialize` has to turn
@@ -21,54 +22,12 @@
21
22
  * handling and outcome routing in one list, where an omitted arm would mean two
22
23
  * different things.
23
24
  */
24
- import { hitlExitModel } from './hitl-routing.js';
25
25
  /**
26
26
  * The author-declared exits of `spec`, or a `NoExits` explaining why there are
27
27
  * none to route.
28
28
  */
29
- export function declaredExits(spec, pinnedVersion) {
29
+ export function declaredExits(spec, _pinnedVersion) {
30
30
  const kind = spec && 'kind' in spec ? spec.kind : undefined;
31
- if (kind === 'hitl') {
32
- const inputs = spec?.inputs;
33
- const model = hitlExitModel(inputs, pinnedVersion);
34
- if (model.shape === 'placeholder') {
35
- // WHY it has no outcome exits decides what the author can do about it, so
36
- // the caller gets the specific one rather than "ports are off".
37
- if ((inputs?.outcomes ?? []).length === 0) {
38
- return { reason: 'it declares no outcomes', suggestion: `outcomes: ['Approve', 'Reject']` };
39
- }
40
- return {
41
- reason: `the '${inputs?.variant}' variant reads its outcomes from its app and leaves on its single 'completed' exit`,
42
- suggestion: `Use .step() and switch on out('<step>', 'Action').`,
43
- };
44
- }
45
- // A quick-form task and a pinned one continue every outcome under `.step()`,
46
- // but their outcome handles are real, so `.stepSwitch` routes them. The
47
- // other fan-out reasons leave nothing to route.
48
- if (model.shape === 'every-outcome' && model.why !== 'quick-form' && model.why !== 'pinned') {
49
- if (model.why === 'outcomePorts: false') {
50
- return {
51
- reason: 'outcomePorts: false continues every outcome to the next step',
52
- suggestion: 'Drop `outcomePorts: false`.',
53
- };
54
- }
55
- if (model.why === 'one outcome') {
56
- return {
57
- reason: 'a single outcome has a single exit, so there is nothing to route',
58
- suggestion: 'Use .step() — the next step follows it unconditionally.',
59
- };
60
- }
61
- return {
62
- reason: 'every outcome ends the process, so no exit distinguishes them',
63
- suggestion: 'Use .step().',
64
- };
65
- }
66
- return model.exits.map((e) => ({
67
- value: e.value,
68
- port: e.port,
69
- ...(e.endsProcess ? { endsProcess: true } : {}),
70
- }));
71
- }
72
31
  return {
73
32
  reason: `a ${kind ?? 'custom'} step declares no author-named exits`,
74
33
  suggestion: 'Use .step(); route failures with .onError().',
@@ -32,19 +32,8 @@ export const TYPED_FACTORIES = {
32
32
  'uipath.agent.autonomous': { factory: 'inlineAgent()' },
33
33
  'uipath.agent.conversational': { factory: 'conversationalAgent()' },
34
34
  'uipath.agent.voice': { factory: 'voiceAgent()' },
35
- 'uipath.conversational.get-conversation-context': { factory: 'conversationContext()' },
36
- 'uipath.conversational.send-message': { factory: 'sendMessage()' },
37
35
  'uipath.conversational.voice.create-outgoing-call': { factory: 'createOutgoingCall()' },
38
36
  'uipath.conversational.voice.end-call': { factory: 'endCall()' },
39
- 'uipath.conversational.wait-for-message': { factory: 'waitForMessage()' },
40
- 'uipath.human-in-the-loop': { factory: 'hitl()' },
41
- 'uipath.human-in-the-loop.coded-action-app': {
42
- factory: "hitl({ variant: 'action-app' })",
43
- },
44
- 'uipath.human-in-the-loop.document-validation': {
45
- factory: "hitl({ variant: 'document-validation' })",
46
- },
47
- 'uipath.human-in-the-loop.quick-form': { factory: "hitl({ variant: 'quick-form' })" },
48
37
  };
49
38
  /**
50
39
  * A reminder, as a `warning`, that a `rawNode` stands in for a typed factory,
package/dist/decompile.js CHANGED
@@ -45,7 +45,7 @@ import { outputBackingGlobal, outputBackingName } from './flow-output-backing.js
45
45
  import { mapJsCode } from './core/js-code-spans.js';
46
46
  import { missingBindingLabels } from './core/binding-messages.js';
47
47
  import { prepareCommand } from './core/cli-spelling.js';
48
- import { OUTCOME_HANDLE_NODE_TYPES, outcomeSlug } from './core/hitl-routing.js';
48
+ import { OUTCOME_HANDLE_NODE_TYPES } from './core/hitl-routing.js';
49
49
  import { STICKY_NOTE_TYPE } from './core/definitionless-node-types.js';
50
50
  import { eventFilterFromTreeLeaf } from './core/event-filters.js';
51
51
  import { sdkFormFieldType } from './core/form-field-type.js';
@@ -226,12 +226,6 @@ const T = {
226
226
  inlineAgent: 'uipath.agent.autonomous',
227
227
  conversationalAgent: 'uipath.agent.conversational',
228
228
  voiceAgent: 'uipath.agent.voice',
229
- hitlQuickForm: 'uipath.human-in-the-loop.quick-form',
230
- hitlActionApp: 'uipath.human-in-the-loop.coded-action-app',
231
- hitlDocValidation: 'uipath.human-in-the-loop.document-validation',
232
- sendMessage: 'uipath.conversational.send-message',
233
- waitForMessage: 'uipath.conversational.wait-for-message',
234
- conversationContext: 'uipath.conversational.get-conversation-context',
235
229
  triggerVoice: 'core.trigger.voice',
236
230
  createOutgoingCall: 'uipath.conversational.voice.create-outgoing-call',
237
231
  endCall: 'uipath.conversational.voice.end-call',
@@ -563,7 +557,7 @@ function registeredImpl(node) {
563
557
  * the node. A node at a version the type does not register is lifted by its
564
558
  * default version's implementation, as every built-in family lifts any version.
565
559
  */
566
- function liftRegistered(node, impl = registeredImpl(node)) {
560
+ function liftRegistered(node, impl = registeredImpl(node), graph) {
567
561
  if (!impl)
568
562
  return undefined;
569
563
  const version = String(node.typeVersion ?? '');
@@ -577,6 +571,8 @@ function liftRegistered(node, impl = registeredImpl(node)) {
577
571
  // when the file carries none (a hand-trimmed or SDK-written file).
578
572
  definition: () => runDefs.get(`${node.type}@${version}`) ?? BUNDLED_DEFINITIONS[`${node.type}@${version}`] ?? runDefs.get(node.type),
579
573
  warn: warnOnce,
574
+ outPorts: () => (graph?.outEdges(node.id) ?? []).map((e) => e.sourcePort),
575
+ convergedFanOut: () => graph?.convergedFanOut(node.id) === true,
580
576
  };
581
577
  const spec = impl.deserialize(node, ctx);
582
578
  return spec === undefined ? undefined : { impl, spec };
@@ -589,8 +585,8 @@ function liftRegistered(node, impl = registeredImpl(node)) {
589
585
  * `{ version }` the type does not register, with a warning. `undefined` when the type is not registered, or the node type declines
590
586
  * the node; the caller's branches take it from there.
591
587
  */
592
- function emitRegisteredSpec(node, imp, inputNames) {
593
- const lifted = liftRegistered(node);
588
+ function emitRegisteredSpec(node, imp, inputNames, graph) {
589
+ const lifted = liftRegistered(node, registeredImpl(node), graph);
594
590
  if (!lifted)
595
591
  return undefined;
596
592
  const { impl, spec } = lifted;
@@ -632,7 +628,7 @@ function emitRegisteredSpec(node, imp, inputNames) {
632
628
  function emitStepSpec(node, imp, inputNames, o, graph) {
633
629
  // A registered node type lifts itself; every other type falls through to the
634
630
  // branches below until its family is ported (FB014's pins are the list).
635
- const lifted = emitRegisteredSpec(node, imp, inputNames);
631
+ const lifted = emitRegisteredSpec(node, imp, inputNames, graph);
636
632
  if (lifted !== undefined)
637
633
  return lifted;
638
634
  switch (node.type) {
@@ -654,52 +650,6 @@ function emitStepSpec(node, imp, inputNames, o, graph) {
654
650
  case T.conversationalAgent:
655
651
  case T.voiceAgent:
656
652
  return emitAgentCluster(node, imp, inputNames, graph);
657
- case T.hitl:
658
- case T.hitlQuickForm:
659
- case T.hitlActionApp:
660
- case T.hitlDocValidation:
661
- return emitHitl(node, imp, inputNames, graph);
662
- case T.sendMessage:
663
- case T.waitForMessage:
664
- case T.conversationContext: {
665
- // The fixed enum fields (`role`/`mimeType`/`from`) come from the
666
- // definition's defaults, so nothing of them is recovered — only what an
667
- // author could have written.
668
- const i = (node.inputs ?? {});
669
- const v = (x) => {
670
- const raw = typeof x === 'string' ? x : String(x?.expression ?? x ?? '');
671
- return raw.startsWith('=js:') ? emitBareExpr(raw.slice(4), imp, inputNames)
672
- : raw.startsWith('$vars.') ? emitBareExpr(raw, imp, inputNames)
673
- : tsLiteral(raw);
674
- };
675
- const num = (x) => {
676
- if (x === undefined || x === null)
677
- return undefined;
678
- const n = Number(x?.expression ?? x);
679
- return Number.isFinite(n) ? String(n) : undefined;
680
- };
681
- const cid = ['conversationId', v(i.conversationId)];
682
- if (node.type === T.sendMessage) {
683
- return `${imp.use('sendMessage')}(${opts([
684
- cid,
685
- ['exchangeId', v(i.exchangeId)],
686
- ['content', v(i.content)],
687
- // The definition defaults `endExchange` to false; only a departure
688
- // from the default is something the author wrote.
689
- ['endExchange', i.endExchange === true ? 'true' : undefined],
690
- ])})`;
691
- }
692
- if (node.type === T.waitForMessage) {
693
- return `${imp.use('waitForMessage')}(${opts([
694
- cid,
695
- ['numExchanges', num(i.numExchanges) === '20' ? undefined : num(i.numExchanges)],
696
- ])})`;
697
- }
698
- return `${imp.use('conversationContext')}(${opts([
699
- cid,
700
- ['exchangeLimit', num(i.exchangeLimit) === '20' ? undefined : num(i.exchangeLimit)],
701
- ])})`;
702
- }
703
653
  default:
704
654
  // A connector action node: `uipath.connector.<key>.<action>` (but NOT the
705
655
  // event nodes `uipath.connector.event.*` / `.trigger.*`, which are triggers).
@@ -1304,184 +1254,6 @@ function emitAgentCluster(node, imp, inputNames, graph) {
1304
1254
  ['source', tsLiteral(plain(i.source) ?? '')],
1305
1255
  ])})`;
1306
1256
  }
1307
- /**
1308
- * A HUMAN TASK — the base node plus its three sub-typed variants.
1309
- *
1310
- * `hitlInputs` is the widest lowering in the serializer, so this inverts it
1311
- * field by field. Three of its outputs are DERIVED and deliberately not
1312
- * recovered: `schema.schemaId` (a stable hash of the flow and node ids),
1313
- * `type: 'quick'` (the base node's discriminator), and the `variable`/`binding`
1314
- * wiring each field row carries — all of them are re-emitted from what comes
1315
- * back here.
1316
- *
1317
- * A value equal to what serialize would have DEFAULTED is omitted rather than
1318
- * recovered: the step's own name for `title`, `'Low'` for `priority`, and the
1319
- * definition's own `inputDefaults.recipient` for delivery. Recovering those
1320
- * would add arguments the author never wrote to every task.
1321
- */
1322
- function emitHitl(node, imp, inputNames, graph) {
1323
- const i = (node.inputs ?? {});
1324
- const schema = (i.schema ?? {});
1325
- const plain = (v) => {
1326
- const raw = unwrapSource(v);
1327
- return raw === undefined || raw === '' ? undefined : raw;
1328
- };
1329
- const variant = node.type === T.hitlQuickForm ? 'quick-form'
1330
- : node.type === T.hitlActionApp ? 'action-app'
1331
- : node.type === T.hitlDocValidation ? 'document-validation'
1332
- : undefined;
1333
- // A field row's binding is a variable PATH (`vars.x`), not an expression —
1334
- // serialize rewrote the author's `$vars.x` on the way out, so put it back.
1335
- const bindingExpr = (v) => {
1336
- const raw = plain(v);
1337
- if (raw === undefined)
1338
- return undefined;
1339
- const js = wireExprBody(raw);
1340
- return emitBareExpr(js.startsWith('vars.') ? `$${js}` : js, imp, inputNames);
1341
- };
1342
- const rows = (Array.isArray(schema.fields) ? schema.fields : []);
1343
- const fields = rows.map((f) => {
1344
- const id = String(f.id ?? '');
1345
- const direction = String(f.direction ?? 'output');
1346
- return opts([
1347
- ['id', tsLiteral(id)],
1348
- // serialize defaults a row's label to its id.
1349
- ['label', typeof f.label === 'string' && f.label !== id ? tsLiteral(f.label) : undefined],
1350
- // The product's own spelling (`"string"`, `"datetime"`, …) → the SDK's (#1143).
1351
- ['type', tsLiteral(sdkFormFieldType(f.type ?? 'text') ?? String(f.type))],
1352
- ['direction', tsLiteral(direction)],
1353
- ['value', direction === 'input' || direction === 'inOut' ? bindingExpr(f.binding) : undefined],
1354
- ['required', f.required === true ? 'true' : undefined],
1355
- ]);
1356
- });
1357
- const outcomeRows = (Array.isArray(schema.outcomes) ? schema.outcomes : []);
1358
- const outcomes = outcomeRows.map((o, idx) => {
1359
- const name = String(o.name ?? '');
1360
- // The file's id comes back only when serialize would not derive it from the
1361
- // name: a designer-drawn task's ids (`outcome-1778015958049`) are not slugs,
1362
- // and merge keeps the ORIGINAL node, so recompiled edges must use its ids
1363
- // (#1130). An SDK-written task's ids are slugs and stay implicit.
1364
- const id = typeof o.id === 'string' && o.id !== '' && o.id !== outcomeSlug(name, idx) ? o.id : undefined;
1365
- // A bare string is the shorthand; `action: 'End'` or a kept id needs the object.
1366
- return o.action === 'End' || id !== undefined
1367
- ? opts([
1368
- ['name', tsLiteral(name)],
1369
- ['action', o.action === 'End' ? tsLiteral('End') : undefined],
1370
- ['id', id !== undefined ? tsLiteral(id) : undefined],
1371
- ])
1372
- : tsLiteral(name);
1373
- });
1374
- // Delivery: only an override comes back. The definition's own default is what
1375
- // serialize emits when the author wrote nothing.
1376
- const def = runDefs.get(`${node.type}@${node.typeVersion}`) ?? runDefs.get(node.type);
1377
- const defaultRecipient = JSON.stringify(def?.inputDefaults?.recipient ?? null);
1378
- const rec = i.recipient;
1379
- let recipientSrc;
1380
- if (rec !== undefined && JSON.stringify(rec) !== defaultRecipient) {
1381
- const assignee = (rec.assignee ?? {});
1382
- const channels = Array.isArray(rec.channels) ? rec.channels : undefined;
1383
- recipientSrc = opts([
1384
- ['channels', channels ? `[${channels.map((c) => tsLiteral(c)).join(', ')}]` : undefined],
1385
- ['assignee', opts([
1386
- ['type', tsLiteral(String(assignee.type ?? 'user'))],
1387
- ['value', typeof assignee.value === 'string' ? tsLiteral(assignee.value) : undefined],
1388
- ['displayName', typeof assignee.displayName === 'string' ? tsLiteral(assignee.displayName) : undefined],
1389
- ])],
1390
- ]);
1391
- }
1392
- // The deployed app that owns the form (action-app, and a custom-render
1393
- // validation station). `appInputBindings` maps the app's own parameter names
1394
- // to `=`-prefixed expressions.
1395
- const app = i.app;
1396
- // `app.inputs` is a VALUE map — what to send the app, keyed by the app's own
1397
- // parameter names — and it lowers to a SIBLING key, `appInputBindings`, whose
1398
- // values are `=vars.…` paths (or literals). Not a type map, and not inside
1399
- // `app`: both are easy to assume and both round-trip wrong.
1400
- const bindings = (i.appInputBindings ?? {});
1401
- const appInputs = Object.keys(bindings).length === 0 ? undefined : objLit(Object.entries(bindings).map(([k, v]) => {
1402
- const raw = String(v ?? '');
1403
- return [k, raw.startsWith('=') ? emitBareExpr(wireExprBody(raw), imp, inputNames) : tsLiteral(raw)];
1404
- }));
1405
- const appSrc = app === undefined ? undefined : opts([
1406
- // `displayName` is serialize's echo of `name`; only `name` is authored.
1407
- ['name', tsText(app.name)],
1408
- ['key', tsText(app.key)],
1409
- ['folderPath', typeof app.folderPath === 'string' && app.folderPath !== '' ? tsLiteral(app.folderPath) : undefined],
1410
- ['appSystemName', app.appSystemName === null ? 'null'
1411
- : typeof app.appSystemName === 'string' ? tsLiteral(app.appSystemName) : undefined],
1412
- ['inputs', appInputs],
1413
- ]);
1414
- // The validation station's own inputs ride as PLAIN `$vars.…` paths. Anything
1415
- // but a `$vars.` reference stays the wire text verbatim.
1416
- const stationExpr = (v) => {
1417
- const raw = plain(v);
1418
- if (raw === undefined)
1419
- return undefined;
1420
- const js = wireExprBody(raw);
1421
- return js.startsWith('$vars.') ? emitBareExpr(js, imp, inputNames) : tsLiteral(raw);
1422
- };
1423
- const render = plain(i.validationRenderType);
1424
- const documentSrc = node.type !== T.hitlDocValidation ? undefined : opts([
1425
- ['extractionResult', stationExpr(i.extractionResult) ?? tsLiteral('')],
1426
- ['storageBucket', plain(i.storageBucket) !== undefined ? tsLiteral(plain(i.storageBucket)) : undefined],
1427
- ['documentId', plain(i.documentId) !== undefined ? tsLiteral(plain(i.documentId)) : undefined],
1428
- // 'standard' is the definition's default.
1429
- ['render', render !== undefined && render !== 'standard' ? tsLiteral(render) : undefined],
1430
- ['taxonomy', stationExpr(i.taxonomy)],
1431
- ]);
1432
- // On the base node the VERSION says it: 1.1 routes per outcome, 1.2 adds the
1433
- // error output (and implies the routing). Everywhere else — quick-form, a
1434
- // base task on 1.0 — the EDGES say it: outcomes that lead to different places
1435
- // are routed per outcome (`outcomePorts: true`, which a pinned 1.0 keeps at
1436
- // 1.0), and outcomes that all lead to one step are a fan-out, which the
1437
- // task's default recompiles to.
1438
- const version = String(node.typeVersion ?? '');
1439
- const exposeError = version === '1.2';
1440
- const outEdges = graph?.outEdges(node.id) ?? [];
1441
- const derivesHandles = OUTCOME_HANDLE_NODE_TYPES.has(String(node.type));
1442
- const outcomePorts = (node.type === T.hitl && version === '1.1')
1443
- || (derivesHandles && !versionRoutesHitl(node) && graph !== undefined && !graph.convergedFanOut(node.id)
1444
- && outEdges.some((e) => e.sourcePort.startsWith('outcome-')));
1445
- // A lone `completed` edge on a task with several outcomes is the shape the
1446
- // designer stopped drawing: before flow-workbench #3512 (09-08) it was
1447
- // unconditioned, so every outcome continued down it; since then Studio Web and
1448
- // the converter remap it to the primary outcome only. Recompiling writes one
1449
- // `outcome-<id>` edge per outcome to the same step — the first meaning — so
1450
- // this is the one place decompile→compile is deliberately not exact. Say so.
1451
- const legacyCompleted = derivesHandles && outcomeIdsOfNode(node).length > 1
1452
- && outEdges.some((e) => e.sourcePort === 'completed');
1453
- // Both a converged fan-out and a legacy lone `completed` mean "every outcome
1454
- // continues to the next step". Say so in the source with `outcomePorts: false`
1455
- // rather than leaving it to the `{ version }` pin, so the intent survives an
1456
- // author dropping the pin.
1457
- const fanOut = !outcomePorts && derivesHandles && !versionRoutesHitl(node) && outcomeIdsOfNode(node).length > 1
1458
- && (legacyCompleted || graph?.convergedFanOut(node.id) === true);
1459
- if (legacyCompleted) {
1460
- warnOnce(`human task ${tsLiteral(node.id)} leaves on the legacy 'completed' handle, which Studio Web no longer draws `
1461
- + `once a task has outcomes (it reads the edge as the primary outcome only). Recompiling emits one `
1462
- + `outcome-<id> edge per outcome to the same step, so every outcome continues — what the edge meant `
1463
- + `before flow-workbench #3512.`);
1464
- }
1465
- const title = plain(i.title);
1466
- const priority = plain(i.priority);
1467
- return `${imp.use('hitl')}(${opts([
1468
- ['variant', variant !== undefined ? tsLiteral(variant) : undefined],
1469
- // serialize defaults the title to the step's own name.
1470
- ['title', title !== undefined && title !== node.id ? tsLiteral(title) : undefined],
1471
- ['priority', priority !== undefined && priority !== 'Low' ? tsLiteral(priority) : undefined],
1472
- ['labels', plain(i.labels) !== undefined ? tsLiteral(plain(i.labels)) : undefined],
1473
- ['recipient', recipientSrc],
1474
- ['app', appSrc],
1475
- ['document', documentSrc],
1476
- // An app task and a validation station own no inline form; an empty array
1477
- // there is serialize's own, not the author's.
1478
- ['fields', fields.length ? `[${fields.join(', ')}]`
1479
- : (variant === 'action-app' || variant === 'document-validation') ? undefined : '[]'],
1480
- ['outcomes', `[${outcomes.join(', ')}]`],
1481
- ['outcomePorts', outcomePorts ? 'true' : fanOut ? 'false' : undefined],
1482
- ['exposeError', exposeError ? 'true' : undefined],
1483
- ])})`;
1484
- }
1485
1257
  /**
1486
1258
  * A connector EVENT subscription, shared by the trigger (`onEvent`) and the
1487
1259
  * mid-flow wait (`waitForEvent`) — the same node body in both places, so the
@@ -9,14 +9,16 @@
9
9
  import { NodeTypeRegistry } from './core/nodes/registry.js';
10
10
  import { hasBundledDefinition } from './flow-definitions.js';
11
11
  import { aiPatternsFlowFamily } from './nodes/ai-patterns/flow/family.js';
12
+ import { conversationFlowFamily } from './nodes/conversation/flow/family.js';
12
13
  import { dataFabricFlowFamily } from './nodes/data-fabric/flow/family.js';
13
14
  import { delayFlowFamily } from './nodes/delay/flow/family.js';
14
15
  import { documentFlowFamily } from './nodes/document/flow/family.js';
16
+ import { hitlFlowFamily } from './nodes/hitl/flow/family.js';
15
17
  import { httpFlowFamily } from './nodes/http/flow/family.js';
16
18
  import { mockFlowFamily } from './nodes/mock/flow/family.js';
17
19
  import { queueFlowFamily } from './nodes/queue/flow/family.js';
18
20
  import { scriptFlowFamily } from './nodes/script/flow/family.js';
19
21
  import { transformFlowFamily } from './nodes/transform/flow/family.js';
20
22
  /** The families, in no significant order. Adding one is the whole of registering it. */
21
- export const FLOW_NODE_FAMILIES = [aiPatternsFlowFamily, dataFabricFlowFamily, delayFlowFamily, documentFlowFamily, httpFlowFamily, mockFlowFamily, queueFlowFamily, scriptFlowFamily, transformFlowFamily];
23
+ export const FLOW_NODE_FAMILIES = [aiPatternsFlowFamily, conversationFlowFamily, dataFabricFlowFamily, delayFlowFamily, documentFlowFamily, hitlFlowFamily, httpFlowFamily, mockFlowFamily, queueFlowFamily, scriptFlowFamily, transformFlowFamily];
22
24
  export const FLOW_NODE_TYPES = new NodeTypeRegistry('flow', FLOW_NODE_FAMILIES, { hasBundledDefinition });
@@ -42,17 +42,40 @@ export interface FlowNodeContribution extends Omit<NodeContribution, 'definition
42
42
  model?: 'definition' | 'omit';
43
43
  /** The forward port the next step's edge leaves from. Default: the definition's default source handle. */
44
44
  port?: string;
45
+ /**
46
+ * Several forward ports, every one of which continues to the next step — a
47
+ * fan-out (a human task that does not route per outcome leaves on each
48
+ * outcome's handle). `endsRun` marks one the step list ends with an End
49
+ * instead (an `action: 'End'` outcome). Wins over `port`.
50
+ */
51
+ fanOut?: ReadonlyArray<{
52
+ readonly port: string;
53
+ readonly endsRun?: boolean;
54
+ }>;
55
+ /**
56
+ * `outputs` REPLACES the definition's `outputDefinition` on the node instead of
57
+ * being merged over it — for an instance whose output slots are its own (a
58
+ * human task's form decides them). Replaced outputs are written verbatim: no
59
+ * `{nodeId}` substitution, which a merged contribution's outputs get.
60
+ */
61
+ replaceOutputs?: boolean;
45
62
  }
46
63
  /** What `serialize` may ask of the serializer, for one placement. */
47
64
  export interface FlowSerializeContext {
48
65
  /** The step name the author gave this node. */
49
66
  readonly step: string;
67
+ /** The id this node is emitted under (rename-aware for a reused subflow copy). */
68
+ readonly nodeId: string;
69
+ /** The flow's id, as the file's top-level `id` carries it. */
70
+ readonly flowId: string;
50
71
  /**
51
72
  * The version being emitted, always set: the author's `{ version }` when the step
52
73
  * names one, else what `select()` or the default chose. It does not say whether
53
74
  * the author pinned it.
54
75
  */
55
76
  readonly version: string;
77
+ /** The author's `{ version }`, when the step names one; undefined when `select()` or the default chose. */
78
+ readonly pinnedVersion: string | undefined;
56
79
  /** Render one value (an `Expr`, or a literal) as the file stores it, with this scope's rename map. */
57
80
  render(value: unknown): unknown;
58
81
  /**
@@ -128,6 +151,17 @@ export interface FlowDeserializeContext {
128
151
  definition(): unknown;
129
152
  /** Report something the lift could not keep. Decompile prints it once. */
130
153
  warn(message: string): void;
154
+ /** The source ports this node's out-edges leave from, in edge order (an empty list outside a graph). */
155
+ outPorts(): readonly string[];
156
+ /**
157
+ * Whether this node is a human task (base or quick-form, not routing by its
158
+ * 1.1/1.2 version) whose every outcome in `inputs.schema.outcomes` is wired, all
159
+ * to ONE target, with no other exit edge: a fan-out that converges, which the
160
+ * source spells as continuing every outcome to the next step rather than as
161
+ * arms. The decompiler's graph answers it from the node's outcomes, not from
162
+ * the type's `exits()`, so it is `false` for any other node.
163
+ */
164
+ convergedFanOut(): boolean;
131
165
  }
132
166
  /** What `print` may ask of the decompiler. Every method returns source text. */
133
167
  export interface FlowPrintContext {
@@ -254,10 +288,20 @@ export interface FlowNodeType<Spec> extends NodeType<Spec, FlowNodeContribution,
254
288
  * and the port each leaves on — or why it has none to route. What
255
289
  * `.stepSwitch()` routes over, what an arm's `value` and a `.stepToList` port
256
290
  * are checked against, and what decompile reads a fully wired node back as.
291
+ * `pinnedVersion` is the author's `{ version }`, when the step names one: a
292
+ * human task's routing depends on whether it is pinned, not only on the version.
257
293
  * Omitted: the type has no author-declared fan-out. The `error` port is never
258
294
  * one of these (`.onError()` routes it).
259
295
  */
260
- exits?(spec: Spec): DeclaredExit[] | NoExits;
296
+ exits?(spec: Spec, pinnedVersion?: string): DeclaredExit[] | NoExits;
297
+ /**
298
+ * Whether `port` is a forward handle the PRODUCT derives for this spec, which
299
+ * the definition does not list — a human task's `outcome-<id>` handles, drawn
300
+ * from its outcomes at every version while the definition declares only
301
+ * `completed`. Serialize refuses a forward port that is neither declared by
302
+ * the definition, an exit of the type, nor derived.
303
+ */
304
+ derivesPort?(spec: Spec, port: string): boolean;
261
305
  }
262
306
  /** A Flow node type with several versions. */
263
307
  export type FlowVersionedNodeType<Spec> = VersionedNodeType<Spec, FlowNodeContribution, FlowContexts, FlowNodeType<Spec>>;
@@ -19,9 +19,11 @@
19
19
  export * from './core/expr.js';
20
20
  export * from './core/actions.js';
21
21
  export * from './nodes/ai-patterns/flow/index.js';
22
+ export * from './nodes/conversation/flow/index.js';
22
23
  export * from './nodes/data-fabric/flow/index.js';
23
24
  export * from './nodes/delay/flow/index.js';
24
25
  export * from './nodes/document/flow/index.js';
26
+ export * from './nodes/hitl/flow/index.js';
25
27
  export * from './nodes/http/flow/index.js';
26
28
  export * from './nodes/mock/flow/index.js';
27
29
  export * from './nodes/queue/flow/index.js';
package/dist/flow-sdk.js CHANGED
@@ -23,9 +23,11 @@ export * from './core/expr.js';
23
23
  export * from './core/actions.js';
24
24
  // Node families' public surfaces (docs/NODE_TYPE_ARCHITECTURE_DESIGN.md §8).
25
25
  export * from './nodes/ai-patterns/flow/index.js';
26
+ export * from './nodes/conversation/flow/index.js';
26
27
  export * from './nodes/data-fabric/flow/index.js';
27
28
  export * from './nodes/delay/flow/index.js';
28
29
  export * from './nodes/document/flow/index.js';
30
+ export * from './nodes/hitl/flow/index.js';
29
31
  export * from './nodes/http/flow/index.js';
30
32
  export * from './nodes/mock/flow/index.js';
31
33
  export * from './nodes/queue/flow/index.js';
@@ -26,5 +26,5 @@ export function stepExits(spec, pinnedVersion) {
26
26
  catch {
27
27
  impl = resolveVersion(entry.type, spec, undefined, 'exits');
28
28
  }
29
- return impl.exits?.(spec) ?? declaredExits(spec, pinnedVersion);
29
+ return impl.exits?.(spec, pinnedVersion) ?? declaredExits(spec, pinnedVersion);
30
30
  }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * The conversation family's rules. Moved from check.ts in Phase 4 of
3
+ * docs/NODE_TYPE_ARCHITECTURE_DESIGN.md; codes and messages unchanged.
4
+ */
5
+ import type { DiagnosticCatalog } from '../../../core/nodes/family.js';
6
+ import type { FlowCheckContext } from '../../../flow-node-type.js';
7
+ export declare const CONVERSATION_DIAGNOSTICS: DiagnosticCatalog;
8
+ /**
9
+ * A conversational step (Phase 5C): send-message, wait-for-message, or
10
+ * get-conversation-context.
11
+ *
12
+ * Everything here is decidable offline and invisible later: the node's own
13
+ * schema requires `conversationId` (and `exchangeId`/`content` on send), and
14
+ * the two numeric limits are range-checked by the designer but silently
15
+ * clamped at run time.
16
+ */
17
+ export declare function checkConversational(kind: 'sendMessage' | 'waitForMessage' | 'conversationContext', inputs: Record<string, unknown> | undefined, ctx: FlowCheckContext): void;