@uipath/maestro-builder-sdk 6.16.14 → 6.17.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 (60) hide show
  1. package/dist/api-index.md +108 -107
  2. package/dist/api-members.md +295 -291
  3. package/dist/bindings-v2.d.ts +39 -1
  4. package/dist/bindings-v2.js +289 -9
  5. package/dist/bpmn/compile-cli.js +11 -2
  6. package/dist/bpmn/decompile.js +2 -1
  7. package/dist/bpmn/registry-fallback.js +9 -7
  8. package/dist/case/compile-cli.js +16 -6
  9. package/dist/case/decompile-cli.js +2 -1
  10. package/dist/case/decompile.js +3 -2
  11. package/dist/case/merge-cli.js +3 -2
  12. package/dist/check.js +219 -37
  13. package/dist/cli-result.d.ts +9 -0
  14. package/dist/cli-result.js +12 -0
  15. package/dist/compile-cli.js +78 -53
  16. package/dist/core/actions.d.ts +70 -16
  17. package/dist/core/agentic-process-forms.d.ts +51 -0
  18. package/dist/core/agentic-process-forms.js +49 -0
  19. package/dist/core/binding-messages.d.ts +5 -0
  20. package/dist/core/comment-text.d.ts +29 -0
  21. package/dist/core/comment-text.js +40 -0
  22. package/dist/core/contribution-bindings.d.ts +73 -0
  23. package/dist/core/contribution-bindings.js +105 -0
  24. package/dist/core/expr-check.js +7 -2
  25. package/dist/core/json-file.d.ts +1 -0
  26. package/dist/core/json-file.js +12 -0
  27. package/dist/core/script-return.js +4 -1
  28. package/dist/core/typed-raw-node.js +1 -0
  29. package/dist/core/typescript-module.d.ts +2 -0
  30. package/dist/core/typescript-module.js +18 -0
  31. package/dist/core-definitions.json +210 -0
  32. package/dist/decompile-cli.js +2 -1
  33. package/dist/decompile.js +64 -24
  34. package/dist/emit.js +4 -3
  35. package/dist/flow-sdk.d.ts +14 -9
  36. package/dist/flow-sdk.js +14 -4
  37. package/dist/flow-step-tree.d.ts +2 -3
  38. package/dist/flow-step-tree.js +2 -3
  39. package/dist/load.js +1 -1
  40. package/dist/merge-cli.js +3 -2
  41. package/dist/merge.d.ts +2 -1
  42. package/dist/merge.js +0 -0
  43. package/dist/path-within.d.ts +33 -0
  44. package/dist/path-within.js +41 -0
  45. package/dist/registry/archive/extract.js +2 -5
  46. package/dist/registry/curated-overlay.js +3 -2
  47. package/dist/registry/generators/descriptors.js +20 -18
  48. package/dist/registry/generators/triggers.js +4 -4
  49. package/dist/registry/prepare/messages.js +2 -1
  50. package/dist/registry/prepare/source-labels.js +60 -1
  51. package/dist/registry/prepare-cli.js +14 -4
  52. package/dist/registry/pull-cli.js +3 -2
  53. package/dist/registry/uip/client.d.ts +28 -3
  54. package/dist/registry/uip/client.js +95 -34
  55. package/dist/registry/uip/host.d.ts +41 -0
  56. package/dist/registry/uip/host.js +312 -0
  57. package/dist/serialize.d.ts +7 -0
  58. package/dist/serialize.js +390 -270
  59. package/dist/workdir.js +1 -1
  60. package/package.json +3 -3
package/dist/check.js CHANGED
@@ -7,16 +7,20 @@
7
7
  */
8
8
  import { Expr, SCHEDULE_PRESETS, DELAY_PRESETS, VARIANT_OPERATION, parseIxpProjectType, flatNameOf, flatNameSegmentsOf, FLAT_SEP } from './flow-sdk.js';
9
9
  import { FlowNode } from './core/node-classes.js';
10
+ import { planContributionBindings } from './core/contribution-bindings.js';
11
+ import { resolveContributionDefinition } from './serialize.js';
10
12
  import { QUEUE_ITEM_RECORD_SCHEMA } from './core/queue-record.js';
13
+ import { AGENTIC_PROCESS_FORMS } from './core/agentic-process-forms.js';
11
14
  import { checkEventResolution, checkEventSubscription, eventParamsFor, } from './core/event-checks.js';
12
15
  import { checkCookedEscapes } from './flow-expr-check.js';
13
16
  import { FLOW_EXPR_DIALECT, checkBindingLabels, checkBindingPresence, checkConnectorContract as coreCheckConnectorContract, checkConnectorLookups as coreCheckConnectorLookups, checkConnectorSchema as coreCheckConnectorSchema, collectConnectionUses, connectionConnectorConflicts, } from './core/connector-checks.js';
14
17
  import { prepareCommand } from './core/cli-spelling.js';
18
+ import { commentText } from './core/comment-text.js';
15
19
  import { editDistance } from './core/edit-distance.js';
16
20
  import { isAnswerField } from './core/hitl-answers.js';
17
21
  import { hitlDerivesOutcomeHandles, hitlExitModel, hitlRoutesPerOutcome } from './core/hitl-routing.js';
18
22
  import { declaredExits, hasExits } from './core/step-ports.js';
19
- import { childStepLists, stopsAtEnd } from './flow-step-tree.js';
23
+ import { stopsAtEnd } from './flow-step-tree.js';
20
24
  import { FLOW_SCHEMA_VERSIONS } from './format-profile.js';
21
25
  import { compareSchemaVersions, schemaVersionRefusal } from './schema-version.js';
22
26
  import { planFlowGlobals, planSharedInputs, sharedInputRoots } from './flow-variables.js';
@@ -54,9 +58,11 @@ function checkFlow(built, ancestors, opts = {}) {
54
58
  if (maybeCustomTrigger !== undefined && !('kind' in maybeCustomTrigger) && maybeCustomTrigger instanceof FlowNode) {
55
59
  // A custom FlowTrigger subclass reports through its own diagnostics() hook;
56
60
  // the built-in trigger rules key on `kind` and don't apply.
57
- for (const d of maybeCustomTrigger.diagnostics()) {
61
+ const own = maybeCustomTrigger.diagnostics();
62
+ for (const d of own) {
58
63
  diags.push({ level: d.severity, code: d.code, message: d.message, step: 'trigger' });
59
64
  }
65
+ checkContribution(maybeCustomTrigger, built.triggerOptions?.version, 'trigger', own.some((d) => d.severity === 'error'), diags);
60
66
  }
61
67
  else if (built.trigger !== undefined)
62
68
  checkTrigger(built.trigger, diags, opts);
@@ -366,9 +372,11 @@ function checkFlow(built, ancestors, opts = {}) {
366
372
  // don't apply, and it reports through its own diagnostics() hook.
367
373
  const maybeCustom = s.spec;
368
374
  if (!('kind' in maybeCustom) && maybeCustom instanceof FlowNode) {
369
- for (const d of maybeCustom.diagnostics()) {
375
+ const own = maybeCustom.diagnostics();
376
+ for (const d of own) {
370
377
  diags.push({ level: d.severity, code: d.code, message: d.message, step: s.name });
371
378
  }
379
+ checkContribution(maybeCustom, s.options?.version, s.name, own.some((d) => d.severity === 'error'), diags);
372
380
  }
373
381
  if (s.spec.kind === 'transform')
374
382
  checkTransform(s.name, s.spec.inputs, diags, typeOf, triggerId);
@@ -864,9 +872,11 @@ function checkFlow(built, ancestors, opts = {}) {
864
872
  epIds.add(ep.id);
865
873
  const maybeCustomEp = ep.trigger;
866
874
  if (!('kind' in maybeCustomEp) && maybeCustomEp instanceof FlowNode) {
867
- for (const d of maybeCustomEp.diagnostics()) {
875
+ const own = maybeCustomEp.diagnostics();
876
+ for (const d of own) {
868
877
  diags.push({ level: d.severity, code: d.code, message: d.message, step: ep.id });
869
878
  }
879
+ checkContribution(maybeCustomEp, ep.options?.version, ep.id, own.some((d) => d.severity === 'error'), diags);
870
880
  }
871
881
  else {
872
882
  checkTrigger(ep.trigger, diags, opts, ep.id);
@@ -1283,13 +1293,10 @@ function checkFlow(built, ancestors, opts = {}) {
1283
1293
  }
1284
1294
  }
1285
1295
  }
1286
- // Walk for a `.return()` rather than stringifying the step tree: a subflow
1287
- // step carries a whole child flow, so the tree can be cyclic (a flow that
1288
- // runs itself — SUBFLOW_CYCLE) and always contains the CHILD's returns, which
1289
- // say nothing about whether THIS flow terminates.
1290
- const hasTerminal = hasReturn(built.steps);
1291
- if (!hasTerminal)
1292
- diags.push({ level: 'warning', code: 'NO_RETURN', message: 'Flow has no return(); a default End node will be appended.' });
1296
+ // The End compile appends when the root path runs out of steps (#941).
1297
+ const fallOff = checkFallOffEnd(built);
1298
+ if (fallOff)
1299
+ diags.push(fallOff);
1293
1300
  // Check every child flow with the same rules, in its OWN scope — a child's
1294
1301
  // steps never see the parent's variables, so its undeclared references,
1295
1302
  // duplicate step names and type mismatches are its own. Diagnostics come back
@@ -1376,9 +1383,124 @@ function checkLoopListEnvelope(loop, connectors, opts, diags) {
1376
1383
  suggestion,
1377
1384
  });
1378
1385
  }
1379
- /** True if any step on THIS flow's own tree is a `.return()`. */
1380
- function hasReturn(steps) {
1381
- return steps.some((s) => s.kind === 'return' || childStepLists(s).some(hasReturn));
1386
+ /**
1387
+ * NO_RETURN: a path that runs off the end of the flow, where a declared output
1388
+ * is never written (#941).
1389
+ *
1390
+ * It asks serialize's question, not "is there a `.return()` anywhere?".
1391
+ * serialize appends an End exactly when the root walk has tails left
1392
+ * (`if (finalTails.length)`), and `armTails` mirrors that walk tail for tail.
1393
+ * So a `.terminate()` path appends no End, and a branch where one arm returns
1394
+ * and the other falls off DOES append one.
1395
+ *
1396
+ * The appended End maps every output from its own variable
1397
+ * (`result <- $vars.result`), the same as a `.return()` that names no value.
1398
+ * That loses nothing when the flow declares no outputs, or when each output
1399
+ * was set on the way: by a `default`, a `{ shared }` input that every root
1400
+ * feeds, or `{ updates }` on a step of that path. The warning names the outputs
1401
+ * that are not, and the steps the path falls off from.
1402
+ *
1403
+ * Not covered: the private End a `.stepToList` body gets (#742). `armTails`
1404
+ * skips those bodies, and that End is a different emission site with no
1405
+ * output mappings at all.
1406
+ */
1407
+ function checkFallOffEnd(built) {
1408
+ if (built.outputs.length === 0)
1409
+ return undefined;
1410
+ const from = built.triggerId ?? 'start';
1411
+ if (armTails(built.steps, [{ from }]).length === 0)
1412
+ return undefined;
1413
+ const preset = new Set(built.outputs.filter((o) => o.default !== undefined).map((o) => o.name));
1414
+ // A shared input sets its variable only on runs that enter through a root
1415
+ // feeding it, so it counts as preset only when every root does.
1416
+ const roots = sharedInputRoots(built);
1417
+ for (const t of planSharedInputs({ roots, outputs: built.outputs, vars: built.vars }).targets) {
1418
+ if (t.declaredAs === 'output' && roots.every((r) => t.feeds.some((f) => f.root === r.id)))
1419
+ preset.add(t.name);
1420
+ }
1421
+ const tails = writtenTails(built.steps, [{ from, written: preset }], []);
1422
+ const missing = built.outputs.map((o) => o.name).filter((n) => tails.some((t) => !t.written.has(n)));
1423
+ if (missing.length === 0)
1424
+ return undefined;
1425
+ const steps = [...new Set(tails.filter((t) => missing.some((n) => !t.written.has(n))).map((t) => t.from))];
1426
+ const one = missing.length === 1;
1427
+ return {
1428
+ level: 'warning',
1429
+ code: 'NO_RETURN',
1430
+ message: `Path ending at ${steps.map((x) => `"${x}"`).join(', ')} falls into the End compile appends. `
1431
+ + `${one ? 'Output' : 'Outputs'} ${missing.map((n) => `"${n}"`).join(', ')} may be unset there. `
1432
+ + `End the path with .return({ … }) or set ${one ? 'it' : 'them'} with { updates } (a loop body may run zero times).`,
1433
+ step: steps[0],
1434
+ suggestion: `.return({ ${missing.map((n) => `${n}: …`).join(', ')} })`,
1435
+ };
1436
+ }
1437
+ /**
1438
+ * `armTails`, step for step, carrying what each path has surely written. A
1439
+ * `.return()` value is NOT a write here: it lives on that `.return()`'s End,
1440
+ * which is not the End this is about. Where paths meet, only what all of them
1441
+ * wrote counts.
1442
+ *
1443
+ * A `.break()` leaves the enclosing loop with what its path wrote so far, so
1444
+ * its tail goes to `breaks`, and the loop counts it as one of the ways out. A
1445
+ * loop body may run zero times, so it writes nothing for sure. A do-while body
1446
+ * runs at least once, and is left by its end or by a `.break()`, so what all of
1447
+ * those wrote counts. Every `.parallel` arm runs, so what each surely writes
1448
+ * counts.
1449
+ */
1450
+ function writtenTails(steps, incoming, breaks) {
1451
+ let tails = incoming;
1452
+ for (const st of steps) {
1453
+ if (st.kind === 'stepToList')
1454
+ continue;
1455
+ if (st.kind === 'stepToRef') {
1456
+ if (st.port === 'output')
1457
+ tails = [];
1458
+ continue;
1459
+ }
1460
+ if (st.kind === 'break') {
1461
+ breaks.push(...tails);
1462
+ return [];
1463
+ }
1464
+ if (st.kind === 'return' || st.kind === 'terminate')
1465
+ return [];
1466
+ const before = surelyWritten(tails);
1467
+ const written = new Set([...before, ...Object.keys(st.options?.updates ?? {})]);
1468
+ const arm = (body) => writtenTails(body, [{ from: st.name, written }], breaks);
1469
+ if (st.kind === 'action') {
1470
+ tails = actionTails(st.name, st.spec, st.options?.version)
1471
+ .map((t) => ({ from: t.from, written }));
1472
+ }
1473
+ else if (st.kind === 'stepSwitch') {
1474
+ tails = st.cases.flatMap((c) => arm(c.body));
1475
+ }
1476
+ else if (st.kind === 'branch') {
1477
+ tails = [...arm(st.then), ...arm(st.otherwise)];
1478
+ }
1479
+ else if (st.kind === 'switch') {
1480
+ tails = [...st.cases.flatMap((c) => arm(c.body)), ...(st.default !== undefined ? arm(st.default) : [])];
1481
+ }
1482
+ else if (st.kind === 'doWhile') {
1483
+ const exits = [];
1484
+ exits.push(...writtenTails(st.body, [{ from: st.name, written }], exits));
1485
+ surelyWritten(exits).forEach((n) => written.add(n));
1486
+ tails = [{ from: st.name, written }];
1487
+ }
1488
+ else if (st.kind === 'parallel') {
1489
+ for (const body of st.arms)
1490
+ surelyWritten(arm(body)).forEach((n) => written.add(n));
1491
+ tails = [{ from: st.name, written }];
1492
+ }
1493
+ else {
1494
+ tails = [{ from: st.name, written }];
1495
+ }
1496
+ }
1497
+ return tails;
1498
+ }
1499
+ /** What every one of `tails` has written; nothing when there are none. */
1500
+ function surelyWritten(tails) {
1501
+ if (tails.length === 0)
1502
+ return new Set();
1503
+ return new Set([...tails[0].written].filter((n) => tails.every((t) => t.written.has(n))));
1382
1504
  }
1383
1505
  /**
1384
1506
  * Closest of `candidates` to `name`: same word ignoring case, one contained in
@@ -1982,10 +2104,11 @@ function renamedFieldMessage(label, step, oldField) {
1982
2104
  * - a `key` that is not a GUID. The commonest form of this is passing the
1983
2105
  * process's NAME where its release key belongs, which produces a node type
1984
2106
  * the platform cannot resolve to anything.
1985
- * - a `folderPath` that looks like a name, or a `name` that looks like a path —
1986
- * the two are swappable by shape and the resulting `resourceKey` is wrong in a
1987
- * way that only surfaces as a 400 from Orchestrator ("Folder does not exist").
1988
- * That is v1's own 2026-06-26 failure mode on this task.
2107
+ * - a `name` that looks like a path (contains "/"). The two are swappable, and
2108
+ * the resulting `resourceKey` is wrong in a way that only surfaces as a 400
2109
+ * from Orchestrator ("Folder does not exist"). That is v1's own 2026-06-26
2110
+ * failure mode on this task. A single-segment `folderPath` is NOT decidable:
2111
+ * root folders and personal workspaces are one segment, so it is not flagged.
1989
2112
  */
1990
2113
  function checkRpaWorkflow(step, inputs, diags) {
1991
2114
  const at = (code, message, suggestion) => {
@@ -2009,33 +2132,33 @@ function checkRpaWorkflow(step, inputs, diags) {
2009
2132
  at('RPA_NO_NAME', `RPA step "${step}" has no \`name\`. The runtime starts the job by name + folder, not by the key in `
2010
2133
  + `the node type, so without it the process cannot be resolved and the run fails before dispatch.`, `name: 'RPA Workflow'`);
2011
2134
  }
2012
- if (blank(inputs?.folderPath)) {
2135
+ const inSolution = inputs?.location === 'in-solution';
2136
+ if (!inSolution && blank(inputs?.folderPath)) {
2013
2137
  at('RPA_NO_FOLDER', `RPA step "${step}" has no \`folderPath\`. The runtime starts the job by name + folder, so the `
2014
2138
  + `Orchestrator folder holding the process is required.`, `folderPath: 'Shared/uipath-maestro-flow/ProjectEuler RPA'`);
2015
2139
  }
2140
+ if (inSolution && blank(inputs?.projectId)) {
2141
+ at('RPA_LOCAL_NO_PROJECT_ID', `In-solution RPA step "${step}" has no \`projectId\`. Read \`resource.projectKey\` from `
2142
+ + `\`resources/solution_folder/process/process/<project>.json\`.`, `projectId: '<resource.projectKey>'`);
2143
+ }
2144
+ else if (inSolution && !GUID_RE.test(String(inputs.projectId).trim())) {
2145
+ at('RPA_LOCAL_PROJECT_ID_NOT_GUID', `In-solution RPA step "${step}" has projectId ${JSON.stringify(inputs.projectId)}, which is not `
2146
+ + `the sibling project's GUID. Read \`resource.projectKey\` from the generated resource file.`, `projectId: '<resource.projectKey>'`);
2147
+ }
2148
+ if (inSolution && !blank(inputs?.folderPath)) {
2149
+ at('RPA_LOCAL_HAS_FOLDER', `In-solution RPA step "${step}" also sets \`folderPath\` ${JSON.stringify(inputs.folderPath)}. An `
2150
+ + `in-solution sibling has no folder until the solution is deployed — the platform's own manifest `
2151
+ + `writes \`""\` — so the value would be dropped. Remove it, or drop \`location: 'in-solution'\` to `
2152
+ + `call the published copy.`);
2153
+ }
2016
2154
  // The two are swappable by shape, and swapping them is invisible until
2017
2155
  // Orchestrator answers "Folder does not exist or the user does not have access".
2018
2156
  const name = typeof inputs?.name === 'string' ? inputs.name.trim() : '';
2019
- const folder = typeof inputs?.folderPath === 'string' ? inputs.folderPath.trim() : '';
2020
2157
  if (name.includes('/')) {
2021
2158
  at('RPA_NAME_IS_PATH', `RPA step "${step}"'s \`name\` is ${JSON.stringify(inputs.name)}, which is a folder path. \`name\` is `
2022
2159
  + `the process's own name in Orchestrator; the path it lives in goes in \`folderPath\`. Together they `
2023
2160
  + `form the resourceKey "<folderPath>.<name>", so swapping them points the job at nothing.`);
2024
2161
  }
2025
- if (folder && !folder.includes('/') && name && !name.includes('/')) {
2026
- // A single-segment folder is legal ("Shared"), so this is only worth saying
2027
- // when the folder ALSO fails to look like one of the tenant's roots.
2028
- if (!/^(Shared|Personal|My Workspace)$/i.test(folder)) {
2029
- diags.push({
2030
- level: 'warning',
2031
- code: 'RPA_FOLDER_NOT_PATH',
2032
- message: `RPA step "${step}"'s \`folderPath\` is ${JSON.stringify(inputs.folderPath)} — a single segment with no `
2033
- + `"/". Orchestrator folder paths are usually nested (e.g. "Shared/<team>/<solution>"). If this is `
2034
- + `the process's name rather than its folder, the two arguments are swapped.`,
2035
- step,
2036
- });
2037
- }
2038
- }
2039
2162
  }
2040
2163
  /**
2041
2164
  * Check an `apiWorkflow(...)` call.
@@ -2636,10 +2759,33 @@ function checkAgenticProcess(step, inputs, diags) {
2636
2759
  + `the key in the node type, so without it the process cannot be resolved and the run fails before `
2637
2760
  + `dispatch.`, `name: 'ProcurementProcess'`);
2638
2761
  }
2639
- if (blank(inputs?.folderPath)) {
2762
+ const inSolution = inputs?.location === 'in-solution';
2763
+ if (!inSolution && blank(inputs?.folderPath)) {
2640
2764
  at('AGENTIC_NO_FOLDER', `Agentic process step "${step}" has no \`folderPath\`. The runtime starts the job by name + folder, so `
2641
2765
  + `the Orchestrator folder holding the process is required.`, `folderPath: 'Shared/uipath-agents/ProcurementProcess'`);
2642
2766
  }
2767
+ const form = inputs?.form ?? 'bpmn';
2768
+ if (inSolution && form !== 'flow') {
2769
+ at('AGENTIC_LOCAL_FORM_UNVERIFIED', `In-solution agentic process step "${step}" has \`form: ${JSON.stringify(form)}\`. Only \`form: 'flow'\` `
2770
+ + `has been run against an in-solution sibling; the ${JSON.stringify(form)} shape is unproven. Call the `
2771
+ + `published process instead (drop \`location\` and \`projectId\`, set \`folderPath\`), or use `
2772
+ + `\`form: 'flow'\` if the sibling is a Flow.`, `location: 'published', folderPath: 'Shared/<folder>'`);
2773
+ }
2774
+ if (inSolution && blank(inputs?.projectId)) {
2775
+ const folder = (AGENTIC_PROCESS_FORMS[inputs?.form ?? 'bpmn'] ?? AGENTIC_PROCESS_FORMS.bpmn).solutionFolder;
2776
+ at('AGENTIC_LOCAL_NO_PROJECT_ID', `In-solution agentic process step "${step}" has no \`projectId\`. Read \`resource.projectKey\` from `
2777
+ + `\`resources/solution_folder/process/${folder}/<project>.json\`.`, `projectId: '<resource.projectKey>'`);
2778
+ }
2779
+ else if (inSolution && !GUID_RE.test(String(inputs.projectId).trim())) {
2780
+ at('AGENTIC_LOCAL_PROJECT_ID_NOT_GUID', `In-solution agentic process step "${step}" has projectId ${JSON.stringify(inputs.projectId)}, which is not `
2781
+ + `the sibling project's GUID. Read \`resource.projectKey\` from the generated resource file.`, `projectId: '<resource.projectKey>'`);
2782
+ }
2783
+ if (inSolution && !blank(inputs?.folderPath)) {
2784
+ at('AGENTIC_LOCAL_HAS_FOLDER', `In-solution agentic process step "${step}" also sets \`folderPath\` ${JSON.stringify(inputs.folderPath)}. An `
2785
+ + `in-solution sibling has no folder until the solution is deployed — the platform's own manifest `
2786
+ + `writes \`""\` — so the value would be dropped. Remove it, or drop \`location: 'in-solution'\` to `
2787
+ + `call the published copy.`);
2788
+ }
2643
2789
  const name = typeof inputs?.name === 'string' ? inputs.name.trim() : '';
2644
2790
  if (name.includes('/')) {
2645
2791
  at('AGENTIC_NAME_IS_PATH', `Agentic process step "${step}"'s \`name\` is ${JSON.stringify(inputs.name)}, which is a folder path. `
@@ -3642,7 +3788,7 @@ function checkQueueItem(step, inputs, diags) {
3642
3788
  + `Orchestrator rejects a queue item whose property values are not simple: `
3643
3789
  + `"Property values must be simple (like numbers or text) and not complex or composite `
3644
3790
  + `data types (like objects or arrays)". Flatten it, or JSON-encode it into one string `
3645
- + `field.`, `item: { ${k}: JSON.stringify(...) } // or ${k}Id, ${k}Name, …`);
3791
+ + `field.`, `item: { ${k}: JSON.stringify(...) } // or ${commentText(k)}Id, ${commentText(k)}Name, …`);
3646
3792
  }
3647
3793
  }
3648
3794
  }
@@ -3933,7 +4079,7 @@ function fanInDiag(join, arm, steps, tails) {
3933
4079
  + `edge, so the run never gets past "${join}" — with no incident, and \`flow validate\` answers Valid. Add a `
3934
4080
  + `step at the end of the arm${last?.name ? `, after "${last.name}"` : ''}, so the paths meet on that step and `
3935
4081
  + `only its one edge reaches the join, or move human task "${task}" out of the \`.parallel(…)\`.`,
3936
- suggestion: `….step('record', …) // last step of arm ${arm + 1}`,
4082
+ suggestion: `….step('record', …) // last step of arm ${commentText(arm + 1)}`,
3937
4083
  };
3938
4084
  }
3939
4085
  /**
@@ -5623,3 +5769,39 @@ function checkSchemaVersion(built, ancestors, diags) {
5623
5769
  });
5624
5770
  }
5625
5771
  }
5772
+ /**
5773
+ * Run a custom node's contribution lowering DRY, so `check` reports what
5774
+ * `compile` would throw. For instance, a `rawNode` carrying an in-solution
5775
+ * sibling's manifest with no `values` row for `<bindings.folderPath>` fails here
5776
+ * with the same code compile throws.
5777
+ *
5778
+ * It calls `contribute()`, resolves the definition the way serialize does, and
5779
+ * plans the `bindings[]` entries serialize would build
5780
+ * ({@link planContributionBindings}). A name the plan cannot close is
5781
+ * `CONTRIBUTION_BINDING_UNRESOLVED` — the same code and sentence serialize
5782
+ * throws. A contribution that throws is `CONTRIBUTION_INVALID`, unless
5783
+ * `ownError` says the node's own `diagnostics()` already reported an error (a
5784
+ * raw connector node throws for the reason `RAW_NODE_CONNECTOR` already gives).
5785
+ */
5786
+ function checkContribution(node, requestedVersion, step, ownError, diags) {
5787
+ let def;
5788
+ let supplied;
5789
+ try {
5790
+ const contribution = node.contribute(requestedVersion !== undefined ? { requestedVersion } : {});
5791
+ def = resolveContributionDefinition(contribution.definition, requestedVersion, `step "${step}"`);
5792
+ supplied = contribution.bindings;
5793
+ }
5794
+ catch (e) {
5795
+ if (ownError)
5796
+ return;
5797
+ diags.push({
5798
+ level: 'error', code: 'CONTRIBUTION_INVALID', step,
5799
+ message: `compile cannot lower step "${step}": ${e instanceof Error ? e.message : String(e)}`,
5800
+ });
5801
+ return;
5802
+ }
5803
+ const plan = planContributionBindings(def, supplied);
5804
+ if (plan.problems.length === 0)
5805
+ return;
5806
+ diags.push({ level: 'error', code: 'CONTRIBUTION_BINDING_UNRESOLVED', step, message: plan.problems.join(' ') });
5807
+ }
@@ -34,3 +34,12 @@ export declare function reportDiagnostics(diags: ReadonlyArray<{
34
34
  message: string;
35
35
  suggestion?: string;
36
36
  }>): void;
37
+ /**
38
+ * The error diagnostic for a throw that ends a compile: the error's own string
39
+ * `code` when it carries one, else `COMPILE_FAILED`.
40
+ */
41
+ export declare function thrownDiagnostic(error: unknown): {
42
+ level: 'error';
43
+ code: string;
44
+ message: string;
45
+ };
@@ -47,3 +47,15 @@ export function reportDiagnostics(diags) {
47
47
  diagnostics: diags.map((d) => ({ level: d.level, code: d.code, message: d.message, ...(d.suggestion ? { suggestion: d.suggestion } : {}) })),
48
48
  });
49
49
  }
50
+ /**
51
+ * The error diagnostic for a throw that ends a compile: the error's own string
52
+ * `code` when it carries one, else `COMPILE_FAILED`.
53
+ */
54
+ export function thrownDiagnostic(error) {
55
+ const code = error?.code;
56
+ return {
57
+ level: 'error',
58
+ code: typeof code === 'string' && code.length > 0 ? code : 'COMPILE_FAILED',
59
+ message: error instanceof Error ? error.message : String(error),
60
+ };
61
+ }
@@ -17,8 +17,9 @@
17
17
  * product's own gates are supposed to answer. `.build()` still throws on
18
18
  * structural problems; only the diagnostic pass is skipped.
19
19
  */
20
- import { reportDiagnostics, reportResult } from './cli-result.js';
21
- import { writeFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:fs';
20
+ import { reportDiagnostics, reportResult, thrownDiagnostic } from './cli-result.js';
21
+ import { writeJsonFile } from './core/json-file.js';
22
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:fs';
22
23
  import { basename, dirname, join, resolve } from 'node:path';
23
24
  import { resolveFlowFile, loadBuiltFlow } from './load.js';
24
25
  import { serializeWithSidecars, connectorInputErrors, compileOwnsResource } from './serialize.js';
@@ -26,9 +27,10 @@ import { check } from './check.js';
26
27
  import { byoaCheckOpts, eventResolveErrorFor, openCliLibrary } from './cli-library.js';
27
28
  import { Bindings } from './bindings.js';
28
29
  import { flagValue, runWhenInvokedDirectly } from './cli-run.js';
29
- import { connectionResources, mergeBindingsV2, readBindingsV2 } from './bindings-v2.js';
30
+ import { connectionResources, inlineAgentResources, mergeBindingsV2, mergeInlineAgentResources, readBindingsV2, readInlineAgentFiles, } from './bindings-v2.js';
30
31
  import { ensureTypeScriptRuntime } from './node-runtime.js';
31
32
  import { defaultBindingsFile } from './workdir.js';
33
+ import { isWithin } from './path-within.js';
32
34
  import { BindingRefusal } from './core/binding-messages.js';
33
35
  import { ByoaRefusal } from './core/byoa.js';
34
36
  export async function run(argv) {
@@ -65,6 +67,8 @@ export async function run(argv) {
65
67
  const library = openCliLibrary(argv);
66
68
  const bindFile = flagValue(argv, '--bindings') ?? defaultBindingsFile();
67
69
  const bindings = new Bindings(bindFile);
70
+ // One list per run; every exit path reports it (reportResult overwrites keys).
71
+ const diagnostics = [];
68
72
  if (!noCheck) {
69
73
  const diags = check(built, library
70
74
  ? {
@@ -100,8 +104,9 @@ export async function run(argv) {
100
104
  const errors = diags.filter((d) => d.level === 'error');
101
105
  for (const d of diags)
102
106
  console.error(` ${d.level === 'error' ? '✗' : '⚠'} [${d.code}] ${d.message}${d.suggestion ? ` (did you mean "${d.suggestion}"?)` : ''}`);
107
+ diagnostics.push(...diags);
103
108
  if (errors.length) {
104
- reportDiagnostics(diags);
109
+ reportDiagnostics(diagnostics);
105
110
  console.error(`compile: ${errors.length} error(s); not emitting.`);
106
111
  return 1;
107
112
  }
@@ -121,28 +126,27 @@ export async function run(argv) {
121
126
  emitted = serializeWithSidecars(built, { library, bindings, warn: (w) => warnings.push(w) });
122
127
  }
123
128
  catch (error) {
124
- if (!(error instanceof BindingRefusal || error instanceof ByoaRefusal))
125
- throw error;
126
129
  for (const w of warnings)
127
130
  console.error(` ⚠ [${w.code}] ${w.message}`);
131
+ diagnostics.push(...warnings.map(serializeWarningDiagnostic));
132
+ if (!(error instanceof BindingRefusal || error instanceof ByoaRefusal)) {
133
+ // Any other throw still ends the run through the caller (exit 1, `--json`
134
+ // `error`); the list is reported first.
135
+ diagnostics.push(thrownDiagnostic(error));
136
+ reportDiagnostics(diagnostics);
137
+ throw error;
138
+ }
128
139
  console.error(` ✗ [${error.code}] ${error.message}`);
129
- reportDiagnostics([
130
- ...warnings.map((w) => ({ level: 'warning', code: w.code, message: w.message })),
131
- { level: 'error', code: error.code, message: error.message },
132
- ]);
140
+ diagnostics.push({ level: 'error', code: error.code, message: error.message });
141
+ reportDiagnostics(diagnostics);
133
142
  console.error('compile: 1 error(s); not emitting.');
134
143
  return 1;
135
144
  }
136
145
  const { flow: flowJson, sidecars } = emitted;
137
146
  for (const w of warnings)
138
147
  console.error(` ⚠ [${w.code}] ${w.message}`);
148
+ diagnostics.push(...warnings.map(serializeWarningDiagnostic));
139
149
  const out = flagValue(argv, '-o') ?? `${built.name || basename(file).replace(/\.flow\.ts$|\.ts$/, '')}.flow`;
140
- writeFileSync(out, JSON.stringify(flowJson, null, 2) + '\n');
141
- console.log(`compile: wrote ${out} (${flowJson.nodes.length} nodes, ${flowJson.edges.length} edges)`);
142
- const written = [];
143
- reportResult({ path: resolve(out), nodes: flowJson.nodes.length, edges: flowJson.edges.length, sidecars: written });
144
- // Same keys and types as a failed compile: `warnings` a count, `diagnostics` the list.
145
- reportDiagnostics(warnings.map((w) => ({ level: 'warning', code: w.code, message: w.message })));
146
150
  // An inline agent is TWO artifacts: the node, and the agent's own configuration
147
151
  // at `<source>/agent.json` — which is where flow-v1's serializer points the
148
152
  // runtime (`entryPoint: "<source>/agent.json"`, resolved relative to the
@@ -151,19 +155,31 @@ export async function run(argv) {
151
155
  // Sidecar paths are relative to the emitted flow, and are confined to its
152
156
  // directory: a `..` or absolute path in one would be a bug here, not author
153
157
  // input, but it is cheap to refuse rather than write outside the project.
158
+ //
159
+ // Every sidecar is checked before anything is written; success is reported
160
+ // only once every file is on disk.
154
161
  const outDir = dirname(resolve(out));
155
- for (const s of sidecars) {
156
- const target = resolve(outDir, s.path);
157
- if (target !== outDir && !target.startsWith(outDir + '/')) {
158
- console.error(`compile: refusing to write a sidecar outside the flow's directory: ${s.path}`);
159
- reportResult({ error: `refusing to write a sidecar outside the flow's directory: ${s.path}` });
160
- return 1;
161
- }
162
+ const targets = sidecars.map((s) => ({ sidecar: s, target: resolve(outDir, s.path) }));
163
+ const escaping = targets.find(({ target }) => target === outDir || !isWithin(outDir, target));
164
+ if (escaping) {
165
+ const message = `refusing to write a sidecar outside the flow's directory: ${escaping.sidecar.path}`;
166
+ console.error(`compile: ${message}; not emitting.`);
167
+ diagnostics.push({ level: 'error', code: 'SIDECAR_OUTSIDE_FLOW_DIR', message });
168
+ reportDiagnostics(diagnostics);
169
+ return 1;
170
+ }
171
+ // Reported before the first write, so a write that throws still carries it.
172
+ reportDiagnostics(diagnostics);
173
+ writeJsonFile(out, flowJson);
174
+ console.log(`compile: wrote ${out} (${flowJson.nodes.length} nodes, ${flowJson.edges.length} edges)`);
175
+ const written = [];
176
+ for (const { sidecar, target } of targets) {
162
177
  mkdirSync(dirname(target), { recursive: true });
163
- writeFileSync(target, JSON.stringify(s.json, null, 2) + '\n');
164
- console.log(`compile: wrote ${s.path}`);
178
+ writeJsonFile(target, sidecar.json);
179
+ console.log(`compile: wrote ${sidecar.path}`);
165
180
  written.push(target);
166
181
  }
182
+ reportResult({ path: resolve(out), nodes: flowJson.nodes.length, edges: flowJson.edges.length, sidecars: written });
167
183
  // PRUNE the agent resources this compile owns but did not write this time.
168
184
  // Compile otherwise only ever ADDS files, and an agent resource is keyed by a
169
185
  // uuid derived from the step name and the resource's identity — so dropping a
@@ -180,13 +196,14 @@ export async function run(argv) {
180
196
  // families the SDK cannot into the same directory (including `internal`
181
197
  // built-ins such as `load-attachments`), so anything else found there is
182
198
  // someone else's and is left alone.
183
- const staleBindings = [];
199
+ const removedResources = [];
184
200
  const owned = new Set(sidecars
185
201
  .filter((s) => /\/resources\/[^/]+\/resource\.json$/.test(s.path))
186
202
  .map((s) => resolve(outDir, s.path)));
187
- for (const agentDir of new Set(sidecars
188
- .filter((s) => s.path.endsWith('/agent.json'))
189
- .map((s) => dirname(resolve(outDir, s.path))))) {
203
+ const agentDirs = [...new Set(sidecars
204
+ .filter((s) => s.path.endsWith('/agent.json'))
205
+ .map((s) => dirname(resolve(outDir, s.path))))];
206
+ for (const agentDir of agentDirs) {
190
207
  const resourcesDir = join(agentDir, 'resources');
191
208
  if (!existsSync(resourcesDir))
192
209
  continue;
@@ -203,23 +220,7 @@ export async function run(argv) {
203
220
  }
204
221
  if (!compileOwnsResource(body))
205
222
  continue;
206
- const resourceType = body?.$resourceType;
207
- // The binding `uip agent refresh` derived FROM this file has to go with it.
208
- // Refresh will not retract it: with no resources left it writes nothing, and
209
- // `mergeBindingsV2` preserves every non-connection entry, so the removed
210
- // index would stay declared in `bindings_v2.json` and a deploy could still
211
- // fail on it. Keyed the way refresh keys it — an index by its name.
212
- const named = body?.indexName ?? body?.name;
213
- const key = typeof named === 'string' && named !== '' ? named : undefined;
214
- // A connector tool's binding is a `connection` row, and `mergeBindingsV2`
215
- // re-derives every connection row from the flow below — nothing to retract.
216
- // A built-in (`internal`) tool binds nothing at all: retracting a `process`
217
- // row under its label could only remove a real process tool's binding.
218
- if (key !== undefined && body?.type !== 'integration' && body?.type !== 'internal') {
219
- // Keyed the way refresh keys it: an index by its name, a deployed-resource
220
- // tool under `process` whatever its own type says.
221
- staleBindings.push({ resource: resourceType === 'context' ? 'index' : 'process', key });
222
- }
223
+ removedResources.push(body);
223
224
  rmSync(join(resourcesDir, entry), { recursive: true, force: true });
224
225
  console.log(`compile: removed stale ${join('resources', entry, 'resource.json')}`);
225
226
  }
@@ -235,20 +236,44 @@ export async function run(argv) {
235
236
  const sidecarPath = join(outDir, 'bindings_v2.json');
236
237
  const connections = connectionResources(flowJson);
237
238
  const existing = readBindingsV2(sidecarPath);
239
+ // Each inline agent's own rows (process, index, app, … — #962), derived from
240
+ // the agent directory as it now stands on disk, the way `uip agent refresh
241
+ // --inline-in-flow` derives them. `uip agent validate --inline-in-flow`
242
+ // requires every one of them, so leaving them to a refresh the SDK route
243
+ // never runs made every agent with a deployed-resource tool, a context index
244
+ // or an app escalation fail validate with `AgentValidationDrift`.
245
+ const agentRows = agentDirs
246
+ .map((dir) => readInlineAgentFiles(dir))
247
+ .filter((files) => files !== undefined)
248
+ .map((files) => inlineAgentResources(files));
249
+ // The rows a PRUNED resource declared go with it: refresh never retracts a
250
+ // row, so an index the flow dropped would stay declared and a deploy could
251
+ // still fail on it. Generated by the same function, so they match exactly,
252
+ // and kept when a resource that still exists declares the same row.
253
+ // Connection rows are skipped: every one is re-derived from the flow below.
254
+ const live = new Set(agentRows.flat().map((r) => `${r.resource}\u0000${r.key}`));
255
+ const stale = new Set(inlineAgentResources({ agent: {}, resources: removedResources, features: [] })
256
+ .filter((r) => r.resource !== 'connection' && !live.has(`${r.resource}\u0000${r.key}`))
257
+ .map((r) => `${r.resource}\u0000${r.key}`));
238
258
  // Nothing to declare and nothing already declared — do not create an empty
239
259
  // file for a flow that has no resources at all.
240
- if (connections.length > 0 || existing !== undefined) {
260
+ if (connections.length > 0 || existing !== undefined || agentRows.some((rows) => rows.length > 0)) {
241
261
  const merged = mergeBindingsV2(existing, connections);
242
- if (staleBindings.length > 0) {
243
- const dropped = new Set(staleBindings.map((b) => `${b.resource}\u0000${b.key}`));
244
- merged.resources = merged.resources.filter((r) => !dropped.has(`${r.resource}\u0000${r.key}`));
245
- }
246
- writeFileSync(sidecarPath, JSON.stringify(merged, null, 2) + '\n');
247
- const kept = merged.resources.length - connections.length;
262
+ merged.resources = merged.resources.filter((r) => !stale.has(`${r.resource}\u0000${r.key}`));
263
+ for (const rows of agentRows)
264
+ merged.resources = mergeInlineAgentResources(merged.resources, rows);
265
+ writeJsonFile(sidecarPath, merged);
266
+ const derived = new Set([...connections, ...agentRows.flat()].map((r) => `${r.resource}\u0000${r.key}`));
267
+ const agentCount = merged.resources.filter((r) => r.resource !== 'connection' && derived.has(`${r.resource}\u0000${r.key}`)).length;
268
+ const kept = merged.resources.filter((r) => !derived.has(`${r.resource}\u0000${r.key}`)).length;
248
269
  console.log(`compile: wrote bindings_v2.json (${connections.length} connection resource(s)`
270
+ + `${agentCount > 0 ? `, ${agentCount} inline-agent resource(s)` : ''}`
249
271
  + `${kept > 0 ? `, ${kept} other resource(s) preserved` : ''})`);
250
272
  }
251
273
  }
252
274
  return 0;
253
275
  }
276
+ function serializeWarningDiagnostic(w) {
277
+ return { level: 'warning', code: w.code, message: w.message };
278
+ }
254
279
  runWhenInvokedDirectly(import.meta.url, 'compile', run);