@uipath/maestro-builder-sdk 6.9.3 → 6.9.4

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.
@@ -17,7 +17,7 @@
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 { writeFileSync, existsSync, mkdirSync } from 'node:fs';
20
+ import { writeFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync } from 'node:fs';
21
21
  import { basename, dirname, join, resolve } from 'node:path';
22
22
  import { resolveFlowFile, loadBuiltFlow } from './load.js';
23
23
  import { serializeWithSidecars, connectorInputErrors } from './serialize.js';
@@ -32,6 +32,28 @@ function opt(argv, name) {
32
32
  const i = argv.indexOf(name);
33
33
  return i >= 0 ? argv[i + 1] : undefined;
34
34
  }
35
+ /**
36
+ * The deployed-resource tool `type` values `compile` writes. NOT every
37
+ * `$resourceType: 'tool'`: `uip agent refresh` generates the `integration` ones
38
+ * from the flow's connector nodes into the same directory, so pruning by
39
+ * `$resourceType` alone would delete the generator's own output.
40
+ */
41
+ const COMPILE_OWNED_TOOL_TYPES = new Set([
42
+ 'process', 'api', 'processOrchestration', 'flow', 'agent', 'function',
43
+ ]);
44
+ /**
45
+ * Does `compile` own this resource file — i.e. would this run have written it,
46
+ * so an unwritten one is stale rather than someone else's?
47
+ *
48
+ * Everything it says no to belongs to `uip agent refresh` (connector tools) or to
49
+ * the designer (escalation, mcp, and the kinds the SDK cannot derive).
50
+ */
51
+ function compileOwnsResource(body) {
52
+ const kind = body?.$resourceType;
53
+ if (kind === 'context')
54
+ return true;
55
+ return kind === 'tool' && COMPILE_OWNED_TOOL_TYPES.has(String(body?.type));
56
+ }
35
57
  export async function run(argv) {
36
58
  if (argv.length === 0 || argv[0] === '-h' || argv[0] === '--help') {
37
59
  console.error('usage: compile <flow.ts | BaseName> [-o out.flow] [--library <dir>] [--bindings <file>]');
@@ -152,6 +174,61 @@ export async function run(argv) {
152
174
  writeFileSync(target, JSON.stringify(s.json, null, 2) + '\n');
153
175
  console.log(`compile: wrote ${s.path}`);
154
176
  }
177
+ // PRUNE the agent resources this compile owns but did not write this time.
178
+ // Compile otherwise only ever ADDS files, and an agent resource is keyed by a
179
+ // uuid derived from the step name and the resource's identity — so dropping a
180
+ // `context` from a step, or pointing it at a different index, orphans the old
181
+ // `resources/<uuid>/resource.json` instead of replacing it. Nothing later
182
+ // notices it is orphaned: `uip agent refresh` reads every file under
183
+ // `resources/` and declares it, so `bindings_v2.json` keeps binding an index
184
+ // the flow no longer references, and a deploy can fail on an index that is
185
+ // gone. Measured 2026-09-25 — compile with a context, remove it, recompile:
186
+ // the file survives and refresh still reports `Resources: 1`.
187
+ //
188
+ // Only the types THIS compile emits are pruned. `uip agent refresh` generates
189
+ // connector-tool resources into the same directory, and the designer writes
190
+ // the families the SDK cannot, so anything else found there is someone else's
191
+ // and is left alone.
192
+ const staleBindings = [];
193
+ const owned = new Set(sidecars
194
+ .filter((s) => /\/resources\/[^/]+\/resource\.json$/.test(s.path))
195
+ .map((s) => resolve(outDir, s.path)));
196
+ for (const agentDir of new Set(sidecars
197
+ .filter((s) => s.path.endsWith('/agent.json'))
198
+ .map((s) => dirname(resolve(outDir, s.path))))) {
199
+ const resourcesDir = join(agentDir, 'resources');
200
+ if (!existsSync(resourcesDir))
201
+ continue;
202
+ for (const entry of readdirSync(resourcesDir)) {
203
+ const file = join(resourcesDir, entry, 'resource.json');
204
+ if (!existsSync(file) || owned.has(file))
205
+ continue;
206
+ let body;
207
+ try {
208
+ body = JSON.parse(readFileSync(file, 'utf8'));
209
+ }
210
+ catch {
211
+ continue; // unreadable: not ours to delete
212
+ }
213
+ if (!compileOwnsResource(body))
214
+ continue;
215
+ const resourceType = body?.$resourceType;
216
+ // The binding `uip agent refresh` derived FROM this file has to go with it.
217
+ // Refresh will not retract it: with no resources left it writes nothing, and
218
+ // `mergeBindingsV2` preserves every non-connection entry, so the removed
219
+ // index would stay declared in `bindings_v2.json` and a deploy could still
220
+ // fail on it. Keyed the way refresh keys it — an index by its name.
221
+ const named = body?.indexName ?? body?.name;
222
+ const key = typeof named === 'string' && named !== '' ? named : undefined;
223
+ if (key !== undefined) {
224
+ // Keyed the way refresh keys it: an index by its name, a deployed-resource
225
+ // tool under `process` whatever its own type says.
226
+ staleBindings.push({ resource: resourceType === 'context' ? 'index' : 'process', key });
227
+ }
228
+ rmSync(join(resourcesDir, entry), { recursive: true, force: true });
229
+ console.log(`compile: removed stale ${join('resources', entry, 'resource.json')}`);
230
+ }
231
+ }
155
232
  // `bindings_v2.json` — the solution-resource sidecar `uip solution resources
156
233
  // refresh` reads. Without it the connection is never declared, so the canvas
157
234
  // and the VS Code extension flag the Flow's connection as invalid
@@ -167,6 +244,10 @@ export async function run(argv) {
167
244
  // file for a flow that has no resources at all.
168
245
  if (connections.length > 0 || existing !== undefined) {
169
246
  const merged = mergeBindingsV2(existing, connections);
247
+ if (staleBindings.length > 0) {
248
+ const dropped = new Set(staleBindings.map((b) => `${b.resource}\u0000${b.key}`));
249
+ merged.resources = merged.resources.filter((r) => !dropped.has(`${r.resource}\u0000${r.key}`));
250
+ }
170
251
  writeFileSync(sidecarPath, JSON.stringify(merged, null, 2) + '\n');
171
252
  const kept = merged.resources.length - connections.length;
172
253
  console.log(`compile: wrote bindings_v2.json (${connections.length} connection resource(s)`
@@ -1827,6 +1827,12 @@ export interface ProcessToolRef {
1827
1827
  name: string;
1828
1828
  /** The Orchestrator folder it lives in, e.g. `'Shared'`. */
1829
1829
  folderPath: string;
1830
+ /**
1831
+ * What the tool does. The agent runtime hands this to the MODEL as the tool's
1832
+ * description, so it is how the model decides when to call it — a tool with
1833
+ * only a name is one the model has to guess at.
1834
+ */
1835
+ description?: string;
1830
1836
  /** The resource's input arguments, by name and type — becomes its `inputSchema`. */
1831
1837
  inputs?: Record<string, InlineAgentFieldType>;
1832
1838
  /** What it returns, by name and type — becomes its `outputSchema`. */
@@ -23,10 +23,11 @@ export interface SerializeOpts {
23
23
  warn?: (warning: SerializeWarning) => void;
24
24
  /**
25
25
  * Filled in by `serialize` with the extra files the flow needs beside the
26
- * `.flow`, relative to it. Today that is one `<source>/agent.json` per inline
27
- * agent: the node points at that directory and the platform resolves the agent's
28
- * real configuration there, so a `.flow` shipped without it references a
29
- * definition that is not on disk.
26
+ * `.flow`, relative to it: one `<source>/agent.json` per inline agent, plus a
27
+ * `<source>/resources/<id>/resource.json` for each capability the compiler can
28
+ * derive. The node points at that directory and the platform resolves the
29
+ * agent's real configuration there, so a `.flow` shipped without them references
30
+ * a definition that is not on disk.
30
31
  *
31
32
  * An out-parameter rather than a second return value so no caller that ignores
32
33
  * sidecars has to change — but a caller that WRITES the flow has to write these
package/dist/serialize.js CHANGED
@@ -857,6 +857,172 @@ function renderContextQuery(query, rename) {
857
857
  }
858
858
  return { ...base, promptValue: asText };
859
859
  }
860
+ /**
861
+ * The file extension each retrieval mode accepts by default. The platform's
862
+ * manifest defaults every mode's NODE to `All`, but its resource schema pins
863
+ * `structured`/`batchtransform` to `csv` and `deeprag` to `pdf`/`txt` — so a
864
+ * flat `All` default would emit a resource the platform refuses to parse for
865
+ * every mode but `semantic`.
866
+ */
867
+ const DEFAULT_FILE_EXTENSION = {
868
+ semantic: 'All',
869
+ structured: 'csv',
870
+ deeprag: 'pdf',
871
+ batchtransform: 'csv',
872
+ };
873
+ /**
874
+ * The `resources/<id>/resource.json` an inline agent's context index needs.
875
+ *
876
+ * The node is only half of a grounded agent, the same split as the agent itself
877
+ * (see `inlineAgentJson`). The platform resolves an agent's capabilities from
878
+ * `<source>/resources/<resource-id>/resource.json`, and `uip agent refresh` turns
879
+ * those into the `bindings_v2.json` entries `uip solution resources refresh`
880
+ * syncs. Keyed by the node's `inputs.source`, which is what joins the two halves.
881
+ *
882
+ * On the CLI path the file is the only copy: refresh strips the node and
883
+ * re-derives it from here, so a `.flow` with the node and no file validates clean
884
+ * and leaves the agent ungrounded. (Studio Web is the other direction — its
885
+ * `mapContextResource` builds this file FROM the canvas node.)
886
+ *
887
+ * Shape and enums are the platform's `currentResourceStorageSchema`
888
+ * (`@uipath/agents-storage-schemas`): `description`, `folderPath`, `indexName` and
889
+ * all six `settings` keys are required, `fileExtension` is `{value}` rather than a
890
+ * bare string, and the retrieval mode pins which extensions it accepts — hence the
891
+ * per-mode default here against the node's flat `'All'`. An unparseable resource
892
+ * is worse than none: `uip agent refresh` falls back to `resources: []` and drops
893
+ * every other binding with it.
894
+ */
895
+ function contextResourceJson(cx, spec) {
896
+ const mode = cx.retrievalMode ?? 'semantic';
897
+ return {
898
+ $resourceType: 'context',
899
+ id: spec.id,
900
+ // Null rather than absent: optional in the schema, but every deployed
901
+ // resource carries it, and a designer round-trip writes it back.
902
+ referenceKey: null,
903
+ name: cx.name,
904
+ description: `Context grounding index "${cx.name}".`,
905
+ contextType: 'index',
906
+ // Required by the schema, and load-bearing: this file carries no `indexId`,
907
+ // so `indexName` + `folderPath` is the whole locator, and `createIndexBinding`
908
+ // leaves the folder off the binding when it is empty. `check` warns when the
909
+ // author omits it — an index outside the deployment folder is then unresolvable.
910
+ folderPath: cx.folderPath ?? '',
911
+ indexName: cx.name,
912
+ settings: {
913
+ retrievalMode: mode,
914
+ // The resource's `query` is VARIANT-tagged where the node's is MODE-tagged,
915
+ // and the two vocabularies do not overlap. `prompt` — what a designer types
916
+ // as guidance — is the model-invoked `dynamic`, and its text is the
917
+ // `description` the model reads when deciding what to search for.
918
+ // `text-builder` — an expression the runtime templates — is the pre-fetched
919
+ // `static`, and its text is the `value`. Confirmed behaviourally: switching
920
+ // the SDK's `query` from a string to an expression turned the same agent's
921
+ // retrieval from a tool call into a pre-fetch.
922
+ query: spec.query.mode === 'text-builder'
923
+ ? { variant: 'static', value: String(spec.query.textValue ?? '') }
924
+ : { variant: 'dynamic', description: String(spec.query.promptValue ?? '') },
925
+ folderPathPrefix: { variant: 'static' },
926
+ threshold: cx.threshold ?? 0,
927
+ resultCount: cx.resultCount ?? 3,
928
+ fileExtension: { value: cx.fileExtension ?? DEFAULT_FILE_EXTENSION[mode] ?? 'All' },
929
+ // Mode-specific keys the schema REQUIRES of those two modes and rejects
930
+ // nowhere else, carrying the node's own constants through the enum's
931
+ // spelling (the node says `enabled`; the resource says `Enabled`/`Inline`).
932
+ ...(mode === 'deeprag' ? { citationMode: { value: 'Inline' } } : {}),
933
+ ...(mode === 'batchtransform'
934
+ ? { webSearchGrounding: { value: 'Enabled' }, outputColumns: [] }
935
+ : {}),
936
+ },
937
+ };
938
+ }
939
+ /** A `fieldSchema` object with every declared property marked required. */
940
+ function requireAll(schema) {
941
+ const names = Object.keys(schema.properties);
942
+ return names.length === 0 ? schema : { ...schema, required: names };
943
+ }
944
+ /**
945
+ * The resource `type` each deployed-resource tool kind carries in the agent
946
+ * project, which is NOT its node-type namespace: `maestro` mints
947
+ * `…tool.processorchestration.*` and its resource type is `processOrchestration`.
948
+ * Read off the platform's `currentResourceStorageSchema`; flow-workbench's
949
+ * `CANVAS_TO_STORAGE_TOOL_TYPE` maps them the same way.
950
+ */
951
+ const TOOL_RESOURCE_TYPE = {
952
+ process: 'process', api: 'api', maestro: 'processOrchestration',
953
+ flow: 'flow', agent: 'agent', function: 'function',
954
+ };
955
+ /**
956
+ * The `resources/<id>/resource.json` a DEPLOYED-RESOURCE tool needs — the tool
957
+ * half of what `contextResourceJson` does for an index, and inert for the same
958
+ * reason (see that function).
959
+ *
960
+ * Shape from a real agent project (`WebResearchBriefingAgent`'s `CountSources`,
961
+ * a `kind: 'process'` tool), cross-checked against the platform schema: this
962
+ * family carries `argumentProperties` and `referenceKey` but no `iconUrl`, and
963
+ * `exampleCalls` is present-and-empty. `location: 'solution'` and an empty
964
+ * `properties.folderPath` are the inline-in-flow pair Studio Web's
965
+ * `mapToolResource` writes and uipcli's `agent validate` enforces — the folder
966
+ * itself lives in the flow's top-level `bindings[]`, which
967
+ * `processResourceBindings` already writes for this tool.
968
+ *
969
+ * Returns `undefined` for every OTHER kind. Which kinds get a file is measured,
970
+ * because `uip agent refresh` strips each resource node and re-derives it FROM
971
+ * the file: where the node is `source`-only the round-trip is lossless, and where
972
+ * the node carries the configuration the strip loses it and the rebuild cannot
973
+ * put it back, so writing the file turns a valid flow INVALID.
974
+ *
975
+ * * `ixp`, `clientside` — measured to fail exactly that way (`ixp` leaves the
976
+ * flow failing validate; `clientside` drops the four inputs its node needs).
977
+ * * `connector` — `uip agent refresh` GENERATES it from the flow's connector
978
+ * node, reconstructing a whole `properties` block the SDK should not duplicate.
979
+ * * `mcp`, `a2a` — need the server's `availableTools` and the remote agent's
980
+ * card, which are tenant reads.
981
+ * * `builtin`, `httpRequest` — need a `properties.toolType` that maps
982
+ * one-to-one for `batchtransform` alone; a wrong one selects a different tool.
983
+ */
984
+ function toolResourceJson(tr, spec) {
985
+ const common = {
986
+ $resourceType: 'tool',
987
+ id: spec.id,
988
+ name: tr.name,
989
+ // The runtime hands this to the MODEL as the tool's description, so an empty
990
+ // one is a tool the model has to guess at from its name alone.
991
+ description: tr.description ?? '',
992
+ isEnabled: true,
993
+ // `required` matters here where it does not on the agent's own contract: the
994
+ // model reads this schema to build the call, and an argument the process needs
995
+ // but the schema calls optional fails at run time with nothing said at compile
996
+ // time. The corpus fixture lists every declared argument.
997
+ inputSchema: requireAll(fieldSchema(tr.inputs)),
998
+ outputSchema: fieldSchema(tr.returns),
999
+ // No policies: `inlineAgent`'s tool refs carry none, and the field is
1000
+ // required-but-emptyable.
1001
+ guardrail: { policies: [] },
1002
+ settings: {},
1003
+ argumentProperties: {},
1004
+ };
1005
+ const resourceType = TOOL_RESOURCE_TYPE[tr.kind];
1006
+ if (resourceType !== undefined) {
1007
+ return {
1008
+ ...common,
1009
+ type: resourceType,
1010
+ location: 'solution',
1011
+ // The deployed resource's own key: what joins this tool to the release the
1012
+ // solution declares.
1013
+ referenceKey: tr.key,
1014
+ // `folderPath` is EMPTY on purpose, and the tenant folder is not lost: for
1015
+ // an inline-in-flow agent the folder lives in the flow's own top-level
1016
+ // `bindings[]`, which `processResourceBindings` already writes for this
1017
+ // tool. uipcli's `agent validate` says so where it enforces the pair —
1018
+ // *"Inline-in-flow agents store folder info in the flow's top-level
1019
+ // bindings[] rather than in the tool resource — Studio Web emits an empty
1020
+ // folderPath here"* — and every `inlineAgent` tool is inline by definition.
1021
+ properties: { processName: tr.name, folderPath: '', exampleCalls: [] },
1022
+ };
1023
+ }
1024
+ return undefined;
1025
+ }
860
1026
  /**
861
1027
  * The one TARGET handle every tool resource declares: `input`, artifact-typed,
862
1028
  * fed by the two agent types' `tool` handles. Transcribed from `registry get`
@@ -4496,6 +4662,12 @@ export function serialize(built, opts = {}) {
4496
4662
  // `inputDefaults` are the shape a designer-placed node has — so an omitted
4497
4663
  // option emits the designer's value rather than nothing.
4498
4664
  delete cnode.model;
4665
+ // ONE identity, spent twice: the node's `inputs.source` and the sidecar
4666
+ // resource's `id` are the same uuid, because that is what joins them. The
4667
+ // platform validator says so in its own refusal text — *"the resource id
4668
+ // from the parent inline agent's `resources[]`"*.
4669
+ const contextResourceId = stableUuid(`${built.id}/${step.name}/context/${cx.id}`);
4670
+ const contextQuery = renderContextQuery(cx.query, scope.rename);
4499
4671
  cnode.inputs = {
4500
4672
  // REQUIRED, and the platform's validator is what says so — measured
4501
4673
  // 2026-07-31, omitting it: *"Node type
@@ -4504,7 +4676,7 @@ export function serialize(built, opts = {}) {
4504
4676
  // node"* (MST-9265). It is the resource's own identity inside the
4505
4677
  // project, so it is derived from flow + step + index rather than random,
4506
4678
  // exactly like the agent's `source`: a recompile must not be a diff.
4507
- source: stableUuid(`${built.id}/${step.name}/context/${cx.id}`),
4679
+ source: contextResourceId,
4508
4680
  indexId: cx.id,
4509
4681
  indexName: cx.name,
4510
4682
  ...(cx.folderKey === undefined ? {} : { folderKey: cx.folderKey }),
@@ -4514,7 +4686,7 @@ export function serialize(built, opts = {}) {
4514
4686
  // `{mode, textValue, promptValue, argumentPath}`, with the mode saying
4515
4687
  // which field is live. A plain string is the `prompt` mode; a flow
4516
4688
  // expression is `text-builder`, whose `textValue` the runtime templates.
4517
- query: renderContextQuery(cx.query, scope.rename),
4689
+ query: contextQuery,
4518
4690
  threshold: cx.threshold ?? 0,
4519
4691
  resultCount: cx.resultCount ?? 3,
4520
4692
  fileExtension: cx.fileExtension ?? 'All',
@@ -4525,6 +4697,12 @@ export function serialize(built, opts = {}) {
4525
4697
  };
4526
4698
  scope.nodes.push(cnode);
4527
4699
  addEdge(scope, { node: id, port: 'context' }, cid, 'input');
4700
+ // The other half — without it the node is inert: see
4701
+ // `contextResourceJson`.
4702
+ sidecars.push({
4703
+ path: `${source}/resources/${contextResourceId}/resource.json`,
4704
+ json: contextResourceJson(cx, { id: contextResourceId, query: contextQuery }),
4705
+ });
4528
4706
  }
4529
4707
  // TOOLS — one resource node per tool, joined to the agent's `tool` handle;
4530
4708
  // the same artifact shape as context (no incoming control flow, no output,
@@ -4674,12 +4852,23 @@ export function serialize(built, opts = {}) {
4674
4852
  : tr.kind === 'clientside' ? `clientside.${tr.name}`
4675
4853
  : tr.kind === 'httpRequest' ? `httprequest.${tr.name ?? 'default'}`
4676
4854
  : tr.key;
4855
+ const toolResourceId = stableUuid(`${built.id}/${step.name}/tool/${toolSeed}`);
4677
4856
  tnode.inputs = {
4678
- source: stableUuid(`${built.id}/${step.name}/tool/${toolSeed}`),
4857
+ source: toolResourceId,
4679
4858
  ...extraInputs,
4680
4859
  };
4681
4860
  scope.nodes.push(tnode);
4682
4861
  addEdge(scope, { node: id, port: 'tool' }, tid, 'input');
4862
+ // The other half, for the kinds whose file is DERIVABLE — see
4863
+ // `toolResourceJson` for which are not, and why each is left to the
4864
+ // generator or the designer rather than guessed.
4865
+ const toolResource = toolResourceJson(tr, { id: toolResourceId });
4866
+ if (toolResource !== undefined) {
4867
+ sidecars.push({
4868
+ path: `${source}/resources/${toolResourceId}/resource.json`,
4869
+ json: toolResource,
4870
+ });
4871
+ }
4683
4872
  }
4684
4873
  // ESCALATION — the agent's TOP handle; an Action Center app task a human
4685
4874
  // completes. The node's inputs are the platform's `inputDefaults` shape
@@ -4787,6 +4976,9 @@ export function serialize(built, opts = {}) {
4787
4976
  };
4788
4977
  scope.nodes.push(mnode);
4789
4978
  addEdge(scope, { node: id, port: 'memory' }, mid, 'input');
4979
+ // No `features/<id>/feature.json` until `check` stops refusing memory
4980
+ // (AGENT_MEMORY_NOT_AUTHORABLE); its shape maps cleanly off `AgentMemoryRef`
4981
+ // but cannot be exercised while the handle does not exist.
4790
4982
  }
4791
4983
  for (const t of tails)
4792
4984
  addEdge(scope, t, id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@uipath/maestro-builder-sdk",
3
- "version": "6.9.3",
3
+ "version": "6.9.4",
4
4
  "description": "Build UiPath Flow, Case, and BPMN artifacts by writing TypeScript.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://docs.uipath.com/maestro",
@@ -87,5 +87,5 @@
87
87
  "@types/node": "^22.7.0",
88
88
  "esbuild": "^0.28.1"
89
89
  },
90
- "gitref": "ac3e6768fb9c4f51d37eb6f51c588cbf7b7912c7"
90
+ "gitref": "ca9f8350a07eaf7ec145b315ad37e75e00a23049"
91
91
  }