@uipath/packager-tool-flow 1.201.0-preview.131 → 1.201.0-preview.133

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/agents.d.ts CHANGED
@@ -16,4 +16,9 @@ export declare function buildInlineAgentContract(config: {
16
16
  projectPath: string;
17
17
  outputPath: string;
18
18
  }): Promise<InlineAgentContract>;
19
- export declare const processAgents: (fileSystem: IFileSystem, logger: IToolLogger, projectPath: string, contentFolder: string) => Promise<void>;
19
+ /**
20
+ * Build every root-level agent directory into the content folder and merge
21
+ * their binding resources. Returns each built agent.json keyed by its
22
+ * directory name (the source UUID).
23
+ */
24
+ export declare const processAgents: (fileSystem: IFileSystem, logger: IToolLogger, projectPath: string, contentFolder: string) => Promise<Map<string, Record<string, unknown>>>;
@@ -28,6 +28,17 @@ export declare class FlowTool extends ProjectTool {
28
28
  * of any stale artifacts that may exist on disk.
29
29
  */
30
30
  private generateFlowPackagingArtifacts;
31
+ /**
32
+ * `builtAgents` plus, for each voice node whose agent was not built, the
33
+ * agent.json copied into the content folder. Voice nodes embed the
34
+ * definition into the BPMN, so — unlike text agents — a source missing
35
+ * from the map fails the whole build, not just an entry point.
36
+ * A source with no copied agent.json stays absent;
37
+ * `buildVoiceAgentDefinitions` reports it.
38
+ */
39
+ private resolveVoiceAgents;
40
+ /** Read an `agent.json` copied in without going through `buildAgent`. */
41
+ private readCopiedAgentJson;
31
42
  /**
32
43
  * Copy project files into the correct nupkg structure.
33
44
  *
package/dist/index.d.ts CHANGED
@@ -3,4 +3,6 @@ export { replaceExporterVersion } from "./exporter-version.js";
3
3
  export { createFlowRefResolver, type FlowIoLogger, migrateRawWorkflow, readFlowWorkflow, resolveAndMigrateWorkflow, resolveWorkflowRefs, } from "./flow-io.js";
4
4
  export { FlowTool } from "./flow-tool.js";
5
5
  export { FlowToolFactory } from "./flow-tool-factory.js";
6
- export { setPublishIntentOnInlineAgents } from "./inline-agent-utils.js";
6
+ export { isInlineAgentNodeType, readInlineAgentSource, setPublishIntentOnInlineAgents, } from "./inline-agent-utils.js";
7
+ export { operateRuntimeOptions } from "./operate-runtime-options.js";
8
+ export { assertVoiceAgentDefinitionsEmbedded, type VoiceAgentFlowNode, voiceConvertOptions, } from "./voice-agent-definitions.js";
package/dist/index.js CHANGED
@@ -32,7 +32,7 @@ import {
32
32
  // package.json
33
33
  var package_default = {
34
34
  name: "@uipath/packager-tool-flow",
35
- version: "1.201.0-preview.131",
35
+ version: "1.201.0-preview.133",
36
36
  description: "UiPath Flow tool implementation",
37
37
  type: "module",
38
38
  exports: {
@@ -75,17 +75,17 @@ var package_default = {
75
75
  license: "ISC",
76
76
  peerDependencies: {
77
77
  "@uipath/filesystem": "workspace:*",
78
- "@uipath/flow-converter": "^0.42.1",
79
- "@uipath/flow-core": "^0.68.0",
78
+ "@uipath/flow-converter": "^0.53.6",
79
+ "@uipath/flow-core": "^0.92.5",
80
80
  "@uipath/flow-migrations": "^0.33.0",
81
- "@uipath/flow-schema": "^0.42.0",
81
+ "@uipath/flow-schema": "^0.57.6",
82
82
  "@uipath/solutionpackager-tool-core": "workspace:*",
83
83
  "@uipath/tool-agent": "^2.0.0"
84
84
  },
85
85
  devDependencies: {
86
86
  "@types/node": "^25.5.2",
87
87
  "@uipath/filesystem": "workspace:*",
88
- "@uipath/flow-core": "^0.68.0",
88
+ "@uipath/flow-core": "^0.92.5",
89
89
  "@uipath/solutionpackager-tool-core": "workspace:*",
90
90
  "@uipath/tool-agent": "^2.0.0",
91
91
  "@vitest/browser": "^4.1.6",
@@ -148,6 +148,7 @@ async function buildInlineAgentContract(config) {
148
148
  }
149
149
  var processAgents = async (fileSystem, logger, projectPath, contentFolder) => {
150
150
  const agentResults = [];
151
+ const builtAgents = new Map;
151
152
  const entries = await fileSystem.readdir(projectPath);
152
153
  for (const entry of entries) {
153
154
  const sourceDir = Path2.join(projectPath, entry);
@@ -168,11 +169,13 @@ var processAgents = async (fileSystem, logger, projectPath, contentFolder) => {
168
169
  logger
169
170
  });
170
171
  agentResults.push(result);
172
+ builtAgents.set(entry, result.agent);
171
173
  }
172
174
  const agentBindingResources = agentResults.flatMap(toBindingsResources);
173
175
  if (agentBindingResources.length > 0) {
174
176
  await mergeAgentBindings(fileSystem, contentFolder, agentBindingResources);
175
177
  }
178
+ return builtAgents;
176
179
  };
177
180
 
178
181
  // src/ensure-process-bindings.ts
@@ -431,15 +434,29 @@ async function readUtf8OrNull(fs, filePath) {
431
434
  }
432
435
 
433
436
  // src/inline-agent-utils.ts
434
- var INLINE_AGENT_DEF_TYPES = [
435
- "uipath.agent.autonomous",
436
- "uipath.agent.conversational"
437
- ];
437
+ import { isAgentNodeType } from "@uipath/flow-schema";
438
+ function readInlineAgentSource(node) {
439
+ const candidates = [
440
+ node.inputs?.source,
441
+ node.model?.source,
442
+ node.inputs?.agentProjectId,
443
+ node.model?.agentProjectId
444
+ ];
445
+ for (const candidate of candidates) {
446
+ if (typeof candidate === "string" && candidate.length > 0) {
447
+ return candidate;
448
+ }
449
+ }
450
+ return;
451
+ }
452
+ function isInlineAgentNodeType(type) {
453
+ return isAgentNodeType(type);
454
+ }
438
455
  function setPublishIntentOnInlineAgents(fileFormat) {
439
456
  const ff = fileFormat;
440
457
  for (const def of ff.definitions ?? []) {
441
458
  const d = def;
442
- if (INLINE_AGENT_DEF_TYPES.includes(d.nodeType)) {
459
+ if (isInlineAgentNodeType(d.nodeType)) {
443
460
  const model = d.model ?? {};
444
461
  model.__packageIntent = "Publish";
445
462
  d.model = model;
@@ -447,6 +464,80 @@ function setPublishIntentOnInlineAgents(fileFormat) {
447
464
  }
448
465
  }
449
466
 
467
+ // src/operate-runtime-options.ts
468
+ var CONVERSATION_TRIGGER_NODE_TYPE = "core.trigger.conversation";
469
+ function operateRuntimeOptions(nodes) {
470
+ return nodes.some((node) => node.type === CONVERSATION_TRIGGER_NODE_TYPE) ? { isConversational: true } : undefined;
471
+ }
472
+
473
+ // src/voice-agent-definitions.ts
474
+ import { isVoiceAgentNodeType } from "@uipath/flow-schema";
475
+ function nodeLabel(node) {
476
+ const label = node.display?.label;
477
+ return typeof label === "string" && label.length > 0 ? label : node.id;
478
+ }
479
+ function collectVoiceAgentNodes(nodes) {
480
+ const result = [];
481
+ for (const node of nodes) {
482
+ if (!isVoiceAgentNodeType(node.type))
483
+ continue;
484
+ const source = readInlineAgentSource(node);
485
+ if (!source) {
486
+ throw new Error(`Voice agent node "${nodeLabel(node)}" has no inputs.source. ` + "Set it to the UUID of the agent directory inside the flow project.");
487
+ }
488
+ result.push({ node, source });
489
+ }
490
+ return result;
491
+ }
492
+ function buildVoiceAgentDefinitions(nodes, builtAgents) {
493
+ const definitions = {};
494
+ for (const { node, source } of collectVoiceAgentNodes(nodes)) {
495
+ if (definitions[source])
496
+ continue;
497
+ const agent = builtAgents.get(source);
498
+ if (!agent) {
499
+ throw new Error(`Missing agent definition for voice agent node "${nodeLabel(node)}" (${source}). ` + `Make sure ${source}/agent.json exists in the flow project.`);
500
+ }
501
+ definitions[source] = JSON.stringify({
502
+ ...agent,
503
+ resources: agent.resources ?? [],
504
+ features: agent.features ?? []
505
+ });
506
+ }
507
+ return definitions;
508
+ }
509
+ function assertNoSubflowVoiceAgents(flowJson) {
510
+ const workflow = JSON.parse(flowJson);
511
+ for (const [subflowId, entry] of Object.entries(workflow.subflows ?? {})) {
512
+ for (const node of entry.nodes ?? []) {
513
+ if (!isVoiceAgentNodeType(node.type))
514
+ continue;
515
+ throw new Error(`Voice agent node "${nodeLabel(node)}" is inside subflow "${subflowId}". ` + "Voice agent nodes are not supported inside subflows — move it to the top-level flow.");
516
+ }
517
+ }
518
+ }
519
+ function voiceConvertOptions(config) {
520
+ assertNoSubflowVoiceAgents(config.flowJson);
521
+ const voiceAgentDefinitions = buildVoiceAgentDefinitions(config.nodes, config.builtAgents);
522
+ if (Object.keys(voiceAgentDefinitions).length === 0) {
523
+ return config.convertOptions;
524
+ }
525
+ const options = {
526
+ ...config.convertOptions,
527
+ voiceAgentDefinitions
528
+ };
529
+ return options;
530
+ }
531
+ function assertVoiceAgentDefinitionsEmbedded(bpmn, nodes) {
532
+ const voiceNodes = collectVoiceAgentNodes(nodes);
533
+ if (voiceNodes.length === 0)
534
+ return;
535
+ if (bpmn.includes('name="agentDefinition"'))
536
+ return;
537
+ const labels = voiceNodes.map(({ node }) => `"${nodeLabel(node)}"`).join(", ");
538
+ throw new Error(`Converted BPMN carries no agentDefinition for voice agent node(s) ${labels}. ` + "The installed @uipath/flow-converter does not forward voiceAgentDefinitions " + "to the BPMN serializer — upgrade it. Packaging a voice flow without the " + "embedded agent definition produces a package that deploys but drops every call.");
539
+ }
540
+
450
541
  // src/flow-tool.ts
451
542
  var FlowConstants = {
452
543
  EntryPointsFileName: "entry-points.json",
@@ -475,12 +566,12 @@ class FlowTool extends ProjectTool {
475
566
  this.logger.progress("Copying files...");
476
567
  await this.copyProjectFiles(options.projectPath, contentFolder);
477
568
  this.logger.progress("Processing agents...");
478
- await processAgents(this.fileSystem, this.logger, options.projectPath, contentFolder);
569
+ const builtAgents = await processAgents(this.fileSystem, this.logger, options.projectPath, contentFolder);
479
570
  this.logger.progress("Resolving process bindings...");
480
571
  await this.resolveProcessBindings(contentFolder);
481
572
  const projectId = options.projectStorageId ?? await ensureProjectId(options.projectPath, this.fileSystem);
482
573
  this.logger.progress("Generating packaging artifacts from .flow...");
483
- await this.generateFlowPackagingArtifacts(contentFolder, projectId);
574
+ await this.generateFlowPackagingArtifacts(contentFolder, projectId, builtAgents);
484
575
  this.logger.progress("Creating operate.json file...");
485
576
  await this.createOperateFile(contentFolder, projectId);
486
577
  this.logger.progress("Creating package-descriptor.json file...");
@@ -543,7 +634,7 @@ class FlowTool extends ProjectTool {
543
634
  await this.fileSystem.writeFile(flowPath, JSON.stringify(workflow, null, 4));
544
635
  }
545
636
  }
546
- async generateFlowPackagingArtifacts(contentFolder, projectId) {
637
+ async generateFlowPackagingArtifacts(contentFolder, projectId, builtAgents) {
547
638
  const entries = await this.fileSystem.readdir(contentFolder);
548
639
  const flowFile = entries.find((e) => e.endsWith(".flow"));
549
640
  if (!flowFile) {
@@ -567,8 +658,15 @@ class FlowTool extends ProjectTool {
567
658
  const fileFormat = inMemoryWorkflowToFileFormat(workflow);
568
659
  setPublishIntentOnInlineAgents(fileFormat);
569
660
  const resolvedFlowJson = JSON.stringify(fileFormat);
570
- const { bpmn: rawBpmn } = await convertFlowToBpmn(resolvedFlowJson);
661
+ const flowNodes = workflow.nodes ?? [];
662
+ const voiceAgents = await this.resolveVoiceAgents(flowNodes, builtAgents, contentFolder);
663
+ const { bpmn: rawBpmn } = await convertFlowToBpmn(resolvedFlowJson, voiceConvertOptions({
664
+ flowJson: resolvedFlowJson,
665
+ nodes: flowNodes,
666
+ builtAgents: voiceAgents
667
+ }));
571
668
  const bpmn = replaceExporterVersion(rawBpmn, package_default.version);
669
+ assertVoiceAgentDefinitionsEmbedded(bpmn, flowNodes);
572
670
  await this.fileSystem.writeFile(bpmnPath, bpmn);
573
671
  const packagingNodes = (workflow.nodes ?? []).map((node) => ({
574
672
  id: node.id,
@@ -583,49 +681,24 @@ class FlowTool extends ProjectTool {
583
681
  const definitions = workflow.definitions ?? [];
584
682
  const entryPointsPath = Path3.join(contentFolder, FlowConstants.EntryPointsFileName);
585
683
  const entryPoints = getEntryPoints(bpmnFileName, packagingNodes, variables, definitions, ProjectType.Flow);
586
- const INLINE_AGENT_TYPES = [
587
- "uipath.agent.autonomous",
588
- "uipath.agent.conversational"
589
- ];
590
- const readSource = (n) => {
591
- const inputs = n.inputs;
592
- const model = n.model;
593
- const candidates = [
594
- inputs?.source,
595
- model?.source,
596
- inputs?.agentProjectId,
597
- model?.agentProjectId
598
- ];
599
- for (const c of candidates) {
600
- if (typeof c === "string" && c.length > 0)
601
- return c;
602
- }
603
- return;
604
- };
605
684
  const agentEntryPoints = [];
606
685
  const seenSources = new Set;
607
686
  for (const node of workflow.nodes ?? []) {
608
687
  const n = node;
609
- if (!INLINE_AGENT_TYPES.includes(n.type))
688
+ if (!isInlineAgentNodeType(n.type))
610
689
  continue;
611
- const source = readSource(n);
690
+ const source = readInlineAgentSource(n);
612
691
  if (!source)
613
692
  continue;
614
693
  if (seenSources.has(source))
615
694
  continue;
616
695
  seenSources.add(source);
617
- const agentJsonPath = Path3.join(contentFolder, source, "agent.json");
618
- if (!await this.fileSystem.exists(agentJsonPath))
619
- continue;
620
- let agent = {};
621
- try {
622
- const raw = await this.fileSystem.readFile(agentJsonPath);
623
- if (raw) {
624
- const text = typeof raw === "string" ? raw : new TextDecoder().decode(raw);
625
- agent = JSON.parse(text);
626
- }
627
- } catch {
628
- continue;
696
+ let agent = voiceAgents.get(source);
697
+ if (!agent) {
698
+ agent = await this.readCopiedAgentJson(contentFolder, source);
699
+ if (!agent)
700
+ continue;
701
+ this.logger.warn(`Inline agent "${source}" has no root-level agent directory, so it ` + "was not built. Its entry point comes from the agent.json copied " + "into the package. Move the agent to the project root to get the " + "full build (migration, validation, binding resources).");
629
702
  }
630
703
  const inputSchema = agent.inputSchema ?? {
631
704
  type: "object",
@@ -655,9 +728,39 @@ class FlowTool extends ProjectTool {
655
728
  const startEventMatch = bpmn.match(/<bpmn:startEvent\s+id="([^"]+)"/);
656
729
  const startEventId = startEventMatch?.[1] ?? "start";
657
730
  const mainEntryPoint = `/${bpmnFileName}#${startEventId}`;
658
- await this.fileSystem.writeFile(operatePath, `${JSON.stringify(generateOperateJson(projectId || workflow.id || "", mainEntryPoint), null, 2)}
731
+ const operateJson = generateOperateJson(projectId || workflow.id || "", mainEntryPoint, operateRuntimeOptions(flowNodes));
732
+ await this.fileSystem.writeFile(operatePath, `${JSON.stringify(operateJson, null, 2)}
659
733
  `);
660
734
  }
735
+ async resolveVoiceAgents(flowNodes, builtAgents, contentFolder) {
736
+ const voiceAgents = new Map(builtAgents);
737
+ for (const { source } of collectVoiceAgentNodes(flowNodes)) {
738
+ if (voiceAgents.has(source))
739
+ continue;
740
+ const copied = await this.readCopiedAgentJson(contentFolder, source);
741
+ if (!copied)
742
+ continue;
743
+ voiceAgents.set(source, copied);
744
+ this.logger.warn(`Voice agent "${source}" has no root-level agent directory, so it ` + "was not built. Its embedded definition and entry point come from " + "the agent.json copied into the package. Move the agent to the " + "project root to get the full build (migration, validation, " + "binding resources).");
745
+ }
746
+ return voiceAgents;
747
+ }
748
+ async readCopiedAgentJson(contentFolder, source) {
749
+ const agentJsonPath = Path3.join(contentFolder, source, "agent.json");
750
+ if (!await this.fileSystem.exists(agentJsonPath)) {
751
+ return;
752
+ }
753
+ try {
754
+ const raw = await this.fileSystem.readFile(agentJsonPath);
755
+ if (!raw) {
756
+ return;
757
+ }
758
+ const text = typeof raw === "string" ? raw : new TextDecoder().decode(raw);
759
+ return JSON.parse(text);
760
+ } catch {
761
+ return;
762
+ }
763
+ }
661
764
  async copyProjectFiles(projectPath, contentFolder) {
662
765
  await this.fileSystem.mkdir(contentFolder);
663
766
  const entries = await this.fileSystem.readdir(projectPath);
@@ -735,16 +838,21 @@ class FlowToolFactory {
735
838
  // src/index.ts
736
839
  toolsFactoryRepository.registerProjectToolFactory(new FlowToolFactory);
737
840
  export {
841
+ voiceConvertOptions,
738
842
  setPublishIntentOnInlineAgents,
739
843
  resolveWorkflowRefs,
740
844
  resolveAndMigrateWorkflow,
741
845
  replaceExporterVersion,
846
+ readInlineAgentSource,
742
847
  readFlowWorkflow,
848
+ operateRuntimeOptions,
743
849
  migrateRawWorkflow,
850
+ isInlineAgentNodeType,
744
851
  createFlowRefResolver,
745
852
  buildInlineAgentContract,
853
+ assertVoiceAgentDefinitionsEmbedded,
746
854
  FlowToolFactory,
747
855
  FlowTool
748
856
  };
749
857
 
750
- //# debugId=178D8B52FE90035264756E2164756E21
858
+ //# debugId=7E6DF946A11A786164756E2164756E21
@@ -1,3 +1,15 @@
1
+ /**
2
+ * Resolve the inline-agent's projectId UUID from a flow node. Falls back to
3
+ * the legacy `model.source` / `agentProjectId` aliases so packaging tolerates
4
+ * already-deployed `.flow` files; `model-source-validator.ts` flags those at
5
+ * validate time.
6
+ */
7
+ export declare function readInlineAgentSource(node: {
8
+ inputs?: Record<string, unknown>;
9
+ model?: Record<string, unknown>;
10
+ }): string | undefined;
11
+ /** True for a known inline-agent node type. */
12
+ export declare function isInlineAgentNodeType(type: string | undefined): boolean;
1
13
  /**
2
14
  * Set `__packageIntent = "Publish"` on inline agent definitions in a
3
15
  * file-format workflow so the BPMN converter emits "content/" prefixed
@@ -0,0 +1,10 @@
1
+ /** `runtimeOptions` for a flow's `operate.json`, derived from its nodes. */
2
+ /**
3
+ * A conversation trigger means the flow can be chatted with, so it packs with
4
+ * `isConversational: true`. Nothing else sets it.
5
+ */
6
+ export declare function operateRuntimeOptions(nodes: ReadonlyArray<{
7
+ type?: string;
8
+ }>): {
9
+ isConversational: true;
10
+ } | undefined;
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Voice inline agents ship their built agent definition inside the BPMN
3
+ * (`agentDefinition` on the StartInlineAgentJob activity). The BPMN serializer
4
+ * writes it from the `voiceAgentDefinitions` convert option; a package without
5
+ * it deploys and then drops every call, so
6
+ * {@link assertVoiceAgentDefinitionsEmbedded} fails the build.
7
+ *
8
+ * Callers convert the flow themselves — this module only builds the convert
9
+ * option and checks the result.
10
+ */
11
+ import type { ConvertOptions } from "@uipath/flow-converter";
12
+ /** `ConvertOptions` plus the `voiceAgentDefinitions` map the serializer reads. */
13
+ export interface VoiceAwareConvertOptions extends ConvertOptions {
14
+ voiceAgentDefinitions?: Record<string, string>;
15
+ }
16
+ export interface VoiceAgentFlowNode {
17
+ id: string;
18
+ type: string;
19
+ display?: Record<string, unknown>;
20
+ inputs?: Record<string, unknown>;
21
+ model?: Record<string, unknown>;
22
+ }
23
+ /** Voice agent nodes with their resolved source UUIDs. */
24
+ export declare function collectVoiceAgentNodes(nodes: VoiceAgentFlowNode[]): Array<{
25
+ node: VoiceAgentFlowNode;
26
+ source: string;
27
+ }>;
28
+ /**
29
+ * Build the `voiceAgentDefinitions` map. `builtAgents` holds each source
30
+ * UUID's built agent.json (post `buildAgent`).
31
+ */
32
+ export declare function buildVoiceAgentDefinitions(nodes: VoiceAgentFlowNode[], builtAgents: ReadonlyMap<string, Record<string, unknown>>): Record<string, string>;
33
+ /**
34
+ * The convert options a flow's voice nodes require: the built definitions the
35
+ * serializer embeds, merged onto the caller's own options. Returns
36
+ * `convertOptions` unchanged when the flow has no voice nodes.
37
+ *
38
+ * Throws when a voice node sits in a subflow or its agent is missing — before
39
+ * a BPMN is written, not after.
40
+ */
41
+ export declare function voiceConvertOptions(config: {
42
+ /** The resolved file-format workflow, already stringified. */
43
+ flowJson: string;
44
+ /** The workflow's nodes — scanned for voice agent nodes. */
45
+ nodes: VoiceAgentFlowNode[];
46
+ /** Source UUID → built agent.json for every packaged inline agent. */
47
+ builtAgents: ReadonlyMap<string, Record<string, unknown>>;
48
+ /** The caller's own converter options (e.g. `detached` on debug). */
49
+ convertOptions?: ConvertOptions;
50
+ }): ConvertOptions | undefined;
51
+ /**
52
+ * Whole-document check: one `agentDefinition` anywhere satisfies it. None at
53
+ * all means the installed converter dropped the option, which drops every
54
+ * node at once. Per-node gaps are the serializer's job — it raises on those.
55
+ */
56
+ export declare function assertVoiceAgentDefinitionsEmbedded(bpmn: string, nodes: VoiceAgentFlowNode[]): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uipath/packager-tool-flow",
3
- "version": "1.201.0-preview.131",
3
+ "version": "1.201.0-preview.133",
4
4
  "description": "UiPath Flow tool implementation",
5
5
  "type": "module",
6
6
  "exports": {
@@ -26,12 +26,12 @@
26
26
  "license": "ISC",
27
27
  "peerDependencies": {
28
28
  "@uipath/filesystem": "1.201.0",
29
- "@uipath/flow-converter": "^0.42.1",
30
- "@uipath/flow-core": "^0.68.0",
29
+ "@uipath/flow-converter": "^0.53.6",
30
+ "@uipath/flow-core": "^0.92.5",
31
31
  "@uipath/flow-migrations": "^0.33.0",
32
- "@uipath/flow-schema": "^0.42.0",
32
+ "@uipath/flow-schema": "^0.57.6",
33
33
  "@uipath/solutionpackager-tool-core": "1.201.0",
34
34
  "@uipath/tool-agent": "^2.0.0"
35
35
  },
36
- "gitHead": "5b0a03e4ee4be352459ac9fd82341ff77bc82ee6"
36
+ "gitHead": "7933378ad5276ac369900293a1447474dcec6826"
37
37
  }
package/src/agents.ts CHANGED
@@ -41,13 +41,19 @@ export async function buildInlineAgentContract(config: {
41
41
  };
42
42
  }
43
43
 
44
+ /**
45
+ * Build every root-level agent directory into the content folder and merge
46
+ * their binding resources. Returns each built agent.json keyed by its
47
+ * directory name (the source UUID).
48
+ */
44
49
  export const processAgents = async (
45
50
  fileSystem: IFileSystem,
46
51
  logger: IToolLogger,
47
52
  projectPath: string,
48
53
  contentFolder: string,
49
- ): Promise<void> => {
54
+ ): Promise<Map<string, Record<string, unknown>>> => {
50
55
  const agentResults: AgentBuildResult[] = [];
56
+ const builtAgents = new Map<string, Record<string, unknown>>();
51
57
 
52
58
  const entries = await fileSystem.readdir(projectPath);
53
59
  for (const entry of entries) {
@@ -72,6 +78,7 @@ export const processAgents = async (
72
78
  logger,
73
79
  });
74
80
  agentResults.push(result);
81
+ builtAgents.set(entry, result.agent as Record<string, unknown>);
75
82
  }
76
83
 
77
84
  const agentBindingResources = agentResults.flatMap(toBindingsResources);
@@ -82,4 +89,6 @@ export const processAgents = async (
82
89
  agentBindingResources,
83
90
  );
84
91
  }
92
+
93
+ return builtAgents;
85
94
  };
package/src/flow-tool.ts CHANGED
@@ -39,7 +39,18 @@ import { processAgents } from "./agents.js";
39
39
  import { ensureProcessBindings } from "./ensure-process-bindings.js";
40
40
  import { replaceExporterVersion } from "./exporter-version.js";
41
41
  import { readFlowWorkflow } from "./flow-io.js";
42
- import { setPublishIntentOnInlineAgents } from "./inline-agent-utils.js";
42
+ import {
43
+ isInlineAgentNodeType,
44
+ readInlineAgentSource,
45
+ setPublishIntentOnInlineAgents,
46
+ } from "./inline-agent-utils.js";
47
+ import { operateRuntimeOptions } from "./operate-runtime-options.js";
48
+ import {
49
+ assertVoiceAgentDefinitionsEmbedded,
50
+ collectVoiceAgentNodes,
51
+ type VoiceAgentFlowNode,
52
+ voiceConvertOptions,
53
+ } from "./voice-agent-definitions.js";
43
54
 
44
55
  const FlowConstants = {
45
56
  EntryPointsFileName: "entry-points.json",
@@ -94,7 +105,7 @@ export class FlowTool extends ProjectTool {
94
105
  await this.copyProjectFiles(options.projectPath, contentFolder);
95
106
 
96
107
  this.logger.progress("Processing agents...");
97
- await processAgents(
108
+ const builtAgents = await processAgents(
98
109
  this.fileSystem,
99
110
  this.logger,
100
111
  options.projectPath,
@@ -111,7 +122,11 @@ export class FlowTool extends ProjectTool {
111
122
  this.logger.progress(
112
123
  "Generating packaging artifacts from .flow...",
113
124
  );
114
- await this.generateFlowPackagingArtifacts(contentFolder, projectId);
125
+ await this.generateFlowPackagingArtifacts(
126
+ contentFolder,
127
+ projectId,
128
+ builtAgents,
129
+ );
115
130
 
116
131
  this.logger.progress("Creating operate.json file...");
117
132
  await this.createOperateFile(contentFolder, projectId);
@@ -231,6 +246,7 @@ export class FlowTool extends ProjectTool {
231
246
  private async generateFlowPackagingArtifacts(
232
247
  contentFolder: string,
233
248
  projectId: string,
249
+ builtAgents: ReadonlyMap<string, Record<string, unknown>>,
234
250
  ): Promise<void> {
235
251
  // Find the .flow file in content folder
236
252
  const entries = await this.fileSystem.readdir(contentFolder);
@@ -266,8 +282,28 @@ export class FlowTool extends ProjectTool {
266
282
  const fileFormat = inMemoryWorkflowToFileFormat(workflow);
267
283
  setPublishIntentOnInlineAgents(fileFormat);
268
284
  const resolvedFlowJson = JSON.stringify(fileFormat);
269
- const { bpmn: rawBpmn } = await convertFlowToBpmn(resolvedFlowJson);
285
+
286
+ // `builtAgents` holds only what processAgents built (root-level agent
287
+ // dirs). A voice agent that reached the content folder some other way
288
+ // (a Studio Web download's `content/<uuid>/` layout) still has to
289
+ // embed a definition — a BPMN without one deploys and then drops
290
+ // every call — so fall back to its copied agent.json.
291
+ const flowNodes = (workflow.nodes ?? []) as VoiceAgentFlowNode[];
292
+ const voiceAgents = await this.resolveVoiceAgents(
293
+ flowNodes,
294
+ builtAgents,
295
+ contentFolder,
296
+ );
297
+ const { bpmn: rawBpmn } = await convertFlowToBpmn(
298
+ resolvedFlowJson,
299
+ voiceConvertOptions({
300
+ flowJson: resolvedFlowJson,
301
+ nodes: flowNodes,
302
+ builtAgents: voiceAgents,
303
+ }),
304
+ );
270
305
  const bpmn = replaceExporterVersion(rawBpmn, pkg.version);
306
+ assertVoiceAgentDefinitionsEmbedded(bpmn, flowNodes);
271
307
  await this.fileSystem.writeFile(bpmnPath, bpmn);
272
308
 
273
309
  // Map file-format nodes to flat PackagingNode shape so
@@ -303,66 +339,33 @@ export class FlowTool extends ProjectTool {
303
339
  ProjectType.Flow,
304
340
  );
305
341
 
306
- // Scan for inline agent nodes and add their entry points.
307
- // The source UUID lives at `inputs.source` (canonical post
308
- // flow-core 0.2.50) or `model.source` (legacy). Validation
309
- // accepts both, so packaging must too — otherwise legacy flows
310
- // produce a .nupkg whose entry-points.json silently omits the
311
- // agent, and runtime returns 404 for `StartInlineAgentJob`.
312
- //
313
- // Canonical copy of this resolution rule lives at
314
- // `flow-tool/src/services/packaging-utils.ts:readInlineAgentSource`.
315
- // It's duplicated here (rather than imported) because `flow-tool`
316
- // already depends on this package, so the reverse import would
317
- // be a cycle. Keep both in sync; longer-term the helper should
318
- // move to `@uipath/flow-schema` (an external dep both can share).
319
- const INLINE_AGENT_TYPES = [
320
- "uipath.agent.autonomous",
321
- "uipath.agent.conversational",
322
- ];
323
- const readSource = (n: Record<string, unknown>): string | undefined => {
324
- const inputs = n.inputs as Record<string, unknown> | undefined;
325
- const model = n.model as Record<string, unknown> | undefined;
326
- const candidates: unknown[] = [
327
- inputs?.source,
328
- model?.source,
329
- inputs?.agentProjectId,
330
- model?.agentProjectId,
331
- ];
332
- for (const c of candidates) {
333
- if (typeof c === "string" && c.length > 0) return c;
334
- }
335
- return undefined;
336
- };
342
+ // `readInlineAgentSource` accepts the legacy `model.source` too, the
343
+ // same as validation; otherwise entry-points.json omits the agent and
344
+ // runtime 404s on `StartInlineAgentJob`.
337
345
  const agentEntryPoints: EntryPoint[] = [];
338
346
  const seenSources = new Set<string>();
339
347
  for (const node of workflow.nodes ?? []) {
340
348
  const n = node as Record<string, unknown>;
341
- if (!INLINE_AGENT_TYPES.includes(n.type as string)) continue;
342
- const source = readSource(n);
349
+ if (!isInlineAgentNodeType(n.type as string)) continue;
350
+ const source = readInlineAgentSource(n);
343
351
  if (!source) continue;
344
352
  if (seenSources.has(source)) continue;
345
353
  seenSources.add(source);
346
354
 
347
- const agentJsonPath = Path.join(
348
- contentFolder,
349
- source,
350
- "agent.json",
351
- );
352
- if (!(await this.fileSystem.exists(agentJsonPath))) continue;
353
-
354
- let agent: Record<string, unknown> = {};
355
- try {
356
- const raw = await this.fileSystem.readFile(agentJsonPath);
357
- if (raw) {
358
- const text =
359
- typeof raw === "string"
360
- ? raw
361
- : new TextDecoder().decode(raw);
362
- agent = JSON.parse(text) as Record<string, unknown>;
363
- }
364
- } catch {
365
- continue;
355
+ // `processAgents` only scans root-level agent dirs, so a
356
+ // `content/<uuid>/` layout is copied but never built.
357
+ // `voiceAgents` already carries the copied fallback for voice
358
+ // sources (and their warn), so those don't warn twice here.
359
+ let agent = voiceAgents.get(source);
360
+ if (!agent) {
361
+ agent = await this.readCopiedAgentJson(contentFolder, source);
362
+ if (!agent) continue;
363
+ this.logger.warn(
364
+ `Inline agent "${source}" has no root-level agent directory, so it ` +
365
+ "was not built. Its entry point comes from the agent.json copied " +
366
+ "into the package. Move the agent to the project root to get the " +
367
+ "full build (migration, validation, binding resources).",
368
+ );
366
369
  }
367
370
 
368
371
  const inputSchema = (agent.inputSchema ?? {
@@ -412,12 +415,72 @@ export class FlowTool extends ProjectTool {
412
415
  const startEventMatch = bpmn.match(/<bpmn:startEvent\s+id="([^"]+)"/);
413
416
  const startEventId = startEventMatch?.[1] ?? "start";
414
417
  const mainEntryPoint = `/${bpmnFileName}#${startEventId}`;
418
+ const operateJson = generateOperateJson(
419
+ projectId || workflow.id || "",
420
+ mainEntryPoint,
421
+ operateRuntimeOptions(flowNodes),
422
+ );
415
423
  await this.fileSystem.writeFile(
416
424
  operatePath,
417
- `${JSON.stringify(generateOperateJson(projectId || workflow.id || "", mainEntryPoint), null, 2)}\n`,
425
+ `${JSON.stringify(operateJson, null, 2)}\n`,
418
426
  );
419
427
  }
420
428
 
429
+ /**
430
+ * `builtAgents` plus, for each voice node whose agent was not built, the
431
+ * agent.json copied into the content folder. Voice nodes embed the
432
+ * definition into the BPMN, so — unlike text agents — a source missing
433
+ * from the map fails the whole build, not just an entry point.
434
+ * A source with no copied agent.json stays absent;
435
+ * `buildVoiceAgentDefinitions` reports it.
436
+ */
437
+ private async resolveVoiceAgents(
438
+ flowNodes: VoiceAgentFlowNode[],
439
+ builtAgents: ReadonlyMap<string, Record<string, unknown>>,
440
+ contentFolder: string,
441
+ ): Promise<Map<string, Record<string, unknown>>> {
442
+ const voiceAgents = new Map(builtAgents);
443
+ for (const { source } of collectVoiceAgentNodes(flowNodes)) {
444
+ if (voiceAgents.has(source)) continue;
445
+ const copied = await this.readCopiedAgentJson(
446
+ contentFolder,
447
+ source,
448
+ );
449
+ if (!copied) continue;
450
+ voiceAgents.set(source, copied);
451
+ this.logger.warn(
452
+ `Voice agent "${source}" has no root-level agent directory, so it ` +
453
+ "was not built. Its embedded definition and entry point come from " +
454
+ "the agent.json copied into the package. Move the agent to the " +
455
+ "project root to get the full build (migration, validation, " +
456
+ "binding resources).",
457
+ );
458
+ }
459
+ return voiceAgents;
460
+ }
461
+
462
+ /** Read an `agent.json` copied in without going through `buildAgent`. */
463
+ private async readCopiedAgentJson(
464
+ contentFolder: string,
465
+ source: string,
466
+ ): Promise<Record<string, unknown> | undefined> {
467
+ const agentJsonPath = Path.join(contentFolder, source, "agent.json");
468
+ if (!(await this.fileSystem.exists(agentJsonPath))) {
469
+ return undefined;
470
+ }
471
+ try {
472
+ const raw = await this.fileSystem.readFile(agentJsonPath);
473
+ if (!raw) {
474
+ return undefined;
475
+ }
476
+ const text =
477
+ typeof raw === "string" ? raw : new TextDecoder().decode(raw);
478
+ return JSON.parse(text) as Record<string, unknown>;
479
+ } catch {
480
+ return undefined;
481
+ }
482
+ }
483
+
421
484
  /**
422
485
  * Copy project files into the correct nupkg structure.
423
486
  *
package/src/index.ts CHANGED
@@ -23,6 +23,16 @@ export {
23
23
  } from "./flow-io.js";
24
24
  export { FlowTool } from "./flow-tool.js";
25
25
  export { FlowToolFactory } from "./flow-tool-factory.js";
26
- export { setPublishIntentOnInlineAgents } from "./inline-agent-utils.js";
26
+ export {
27
+ isInlineAgentNodeType,
28
+ readInlineAgentSource,
29
+ setPublishIntentOnInlineAgents,
30
+ } from "./inline-agent-utils.js";
31
+ export { operateRuntimeOptions } from "./operate-runtime-options.js";
32
+ export {
33
+ assertVoiceAgentDefinitionsEmbedded,
34
+ type VoiceAgentFlowNode,
35
+ voiceConvertOptions,
36
+ } from "./voice-agent-definitions.js";
27
37
 
28
38
  toolsFactoryRepository.registerProjectToolFactory(new FlowToolFactory());
@@ -1,7 +1,33 @@
1
- const INLINE_AGENT_DEF_TYPES = [
2
- "uipath.agent.autonomous",
3
- "uipath.agent.conversational",
4
- ];
1
+ import { isAgentNodeType } from "@uipath/flow-schema";
2
+
3
+ /**
4
+ * Resolve the inline-agent's projectId UUID from a flow node. Falls back to
5
+ * the legacy `model.source` / `agentProjectId` aliases so packaging tolerates
6
+ * already-deployed `.flow` files; `model-source-validator.ts` flags those at
7
+ * validate time.
8
+ */
9
+ export function readInlineAgentSource(node: {
10
+ inputs?: Record<string, unknown>;
11
+ model?: Record<string, unknown>;
12
+ }): string | undefined {
13
+ const candidates: unknown[] = [
14
+ node.inputs?.source,
15
+ node.model?.source,
16
+ node.inputs?.agentProjectId,
17
+ node.model?.agentProjectId,
18
+ ];
19
+ for (const candidate of candidates) {
20
+ if (typeof candidate === "string" && candidate.length > 0) {
21
+ return candidate;
22
+ }
23
+ }
24
+ return undefined;
25
+ }
26
+
27
+ /** True for a known inline-agent node type. */
28
+ export function isInlineAgentNodeType(type: string | undefined): boolean {
29
+ return isAgentNodeType(type);
30
+ }
5
31
 
6
32
  /**
7
33
  * Set `__packageIntent = "Publish"` on inline agent definitions in a
@@ -14,7 +40,7 @@ export function setPublishIntentOnInlineAgents(fileFormat: unknown): void {
14
40
  const ff = fileFormat as Record<string, unknown[]>;
15
41
  for (const def of ff.definitions ?? []) {
16
42
  const d = def as Record<string, unknown>;
17
- if (INLINE_AGENT_DEF_TYPES.includes(d.nodeType as string)) {
43
+ if (isInlineAgentNodeType(d.nodeType as string)) {
18
44
  const model = (d.model ?? {}) as Record<string, unknown>;
19
45
  model.__packageIntent = "Publish";
20
46
  d.model = model;
@@ -0,0 +1,15 @@
1
+ /** `runtimeOptions` for a flow's `operate.json`, derived from its nodes. */
2
+
3
+ const CONVERSATION_TRIGGER_NODE_TYPE = "core.trigger.conversation";
4
+
5
+ /**
6
+ * A conversation trigger means the flow can be chatted with, so it packs with
7
+ * `isConversational: true`. Nothing else sets it.
8
+ */
9
+ export function operateRuntimeOptions(
10
+ nodes: ReadonlyArray<{ type?: string }>,
11
+ ): { isConversational: true } | undefined {
12
+ return nodes.some((node) => node.type === CONVERSATION_TRIGGER_NODE_TYPE)
13
+ ? { isConversational: true }
14
+ : undefined;
15
+ }
@@ -0,0 +1,155 @@
1
+ /**
2
+ * Voice inline agents ship their built agent definition inside the BPMN
3
+ * (`agentDefinition` on the StartInlineAgentJob activity). The BPMN serializer
4
+ * writes it from the `voiceAgentDefinitions` convert option; a package without
5
+ * it deploys and then drops every call, so
6
+ * {@link assertVoiceAgentDefinitionsEmbedded} fails the build.
7
+ *
8
+ * Callers convert the flow themselves — this module only builds the convert
9
+ * option and checks the result.
10
+ */
11
+
12
+ import type { ConvertOptions } from "@uipath/flow-converter";
13
+ import { isVoiceAgentNodeType } from "@uipath/flow-schema";
14
+
15
+ import { readInlineAgentSource } from "./inline-agent-utils.js";
16
+
17
+ /** `ConvertOptions` plus the `voiceAgentDefinitions` map the serializer reads. */
18
+ export interface VoiceAwareConvertOptions extends ConvertOptions {
19
+ voiceAgentDefinitions?: Record<string, string>;
20
+ }
21
+
22
+ export interface VoiceAgentFlowNode {
23
+ id: string;
24
+ type: string;
25
+ display?: Record<string, unknown>;
26
+ inputs?: Record<string, unknown>;
27
+ model?: Record<string, unknown>;
28
+ }
29
+
30
+ function nodeLabel(node: VoiceAgentFlowNode): string {
31
+ const label = node.display?.label;
32
+ return typeof label === "string" && label.length > 0 ? label : node.id;
33
+ }
34
+
35
+ /** Voice agent nodes with their resolved source UUIDs. */
36
+ export function collectVoiceAgentNodes(
37
+ nodes: VoiceAgentFlowNode[],
38
+ ): Array<{ node: VoiceAgentFlowNode; source: string }> {
39
+ const result: Array<{ node: VoiceAgentFlowNode; source: string }> = [];
40
+ for (const node of nodes) {
41
+ if (!isVoiceAgentNodeType(node.type)) continue;
42
+ const source = readInlineAgentSource(node);
43
+ if (!source) {
44
+ throw new Error(
45
+ `Voice agent node "${nodeLabel(node)}" has no inputs.source. ` +
46
+ "Set it to the UUID of the agent directory inside the flow project.",
47
+ );
48
+ }
49
+ result.push({ node, source });
50
+ }
51
+ return result;
52
+ }
53
+
54
+ /**
55
+ * Build the `voiceAgentDefinitions` map. `builtAgents` holds each source
56
+ * UUID's built agent.json (post `buildAgent`).
57
+ */
58
+ export function buildVoiceAgentDefinitions(
59
+ nodes: VoiceAgentFlowNode[],
60
+ builtAgents: ReadonlyMap<string, Record<string, unknown>>,
61
+ ): Record<string, string> {
62
+ const definitions: Record<string, string> = {};
63
+ for (const { node, source } of collectVoiceAgentNodes(nodes)) {
64
+ if (definitions[source]) continue;
65
+ const agent = builtAgents.get(source);
66
+ if (!agent) {
67
+ throw new Error(
68
+ `Missing agent definition for voice agent node "${nodeLabel(node)}" (${source}). ` +
69
+ `Make sure ${source}/agent.json exists in the flow project.`,
70
+ );
71
+ }
72
+ definitions[source] = JSON.stringify({
73
+ ...agent,
74
+ resources: (agent.resources as unknown[]) ?? [],
75
+ features: (agent.features as unknown[]) ?? [],
76
+ });
77
+ }
78
+ return definitions;
79
+ }
80
+
81
+ /**
82
+ * Only top-level voice nodes get definitions, so a subflow one would ship a
83
+ * serviceTask with no `agentDefinition`. The CLI validate rule rejects the
84
+ * same flow; both go away when the platform supports it.
85
+ */
86
+ function assertNoSubflowVoiceAgents(flowJson: string): void {
87
+ const workflow = JSON.parse(flowJson) as {
88
+ subflows?: Record<string, { nodes?: VoiceAgentFlowNode[] }>;
89
+ };
90
+ for (const [subflowId, entry] of Object.entries(workflow.subflows ?? {})) {
91
+ for (const node of entry.nodes ?? []) {
92
+ if (!isVoiceAgentNodeType(node.type)) continue;
93
+ throw new Error(
94
+ `Voice agent node "${nodeLabel(node)}" is inside subflow "${subflowId}". ` +
95
+ "Voice agent nodes are not supported inside subflows — move it to the top-level flow.",
96
+ );
97
+ }
98
+ }
99
+ }
100
+
101
+ /**
102
+ * The convert options a flow's voice nodes require: the built definitions the
103
+ * serializer embeds, merged onto the caller's own options. Returns
104
+ * `convertOptions` unchanged when the flow has no voice nodes.
105
+ *
106
+ * Throws when a voice node sits in a subflow or its agent is missing — before
107
+ * a BPMN is written, not after.
108
+ */
109
+ export function voiceConvertOptions(config: {
110
+ /** The resolved file-format workflow, already stringified. */
111
+ flowJson: string;
112
+ /** The workflow's nodes — scanned for voice agent nodes. */
113
+ nodes: VoiceAgentFlowNode[];
114
+ /** Source UUID → built agent.json for every packaged inline agent. */
115
+ builtAgents: ReadonlyMap<string, Record<string, unknown>>;
116
+ /** The caller's own converter options (e.g. `detached` on debug). */
117
+ convertOptions?: ConvertOptions;
118
+ }): ConvertOptions | undefined {
119
+ assertNoSubflowVoiceAgents(config.flowJson);
120
+ const voiceAgentDefinitions = buildVoiceAgentDefinitions(
121
+ config.nodes,
122
+ config.builtAgents,
123
+ );
124
+ if (Object.keys(voiceAgentDefinitions).length === 0) {
125
+ return config.convertOptions;
126
+ }
127
+ const options: VoiceAwareConvertOptions = {
128
+ ...config.convertOptions,
129
+ voiceAgentDefinitions,
130
+ };
131
+ return options;
132
+ }
133
+
134
+ /**
135
+ * Whole-document check: one `agentDefinition` anywhere satisfies it. None at
136
+ * all means the installed converter dropped the option, which drops every
137
+ * node at once. Per-node gaps are the serializer's job — it raises on those.
138
+ */
139
+ export function assertVoiceAgentDefinitionsEmbedded(
140
+ bpmn: string,
141
+ nodes: VoiceAgentFlowNode[],
142
+ ): void {
143
+ const voiceNodes = collectVoiceAgentNodes(nodes);
144
+ if (voiceNodes.length === 0) return;
145
+ if (bpmn.includes('name="agentDefinition"')) return;
146
+ const labels = voiceNodes
147
+ .map(({ node }) => `"${nodeLabel(node)}"`)
148
+ .join(", ");
149
+ throw new Error(
150
+ `Converted BPMN carries no agentDefinition for voice agent node(s) ${labels}. ` +
151
+ "The installed @uipath/flow-converter does not forward voiceAgentDefinitions " +
152
+ "to the BPMN serializer — upgrade it. Packaging a voice flow without the " +
153
+ "embedded agent definition produces a package that deploys but drops every call.",
154
+ );
155
+ }