@uipath/maestro-builder-sdk 6.32.7 → 6.33.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.
@@ -3,6 +3,7 @@ import { tsLiteral, tsNumber } from '../core/ts-literal.js';
3
3
  import { CASE_FORMAT_PROFILE, CASE_SCHEMA_VERSIONS } from './format-profile.js';
4
4
  import { preserveSchemaVersion } from '../schema-version.js';
5
5
  import { ownEntry } from '../core/own-entry.js';
6
+ import { isUnrestorableSlotId } from './preserve.js';
6
7
  const TASK_KIND_METHOD = {
7
8
  process: 'process',
8
9
  agent: 'agent',
@@ -932,6 +933,8 @@ function slaIdentity(sla) {
932
933
  * has that id.
933
934
  * - empty collections — there is nothing to align.
934
935
  * - `nodeOrder` with fewer than two ids — ordering one node is identity.
936
+ * - an `inputs` / `outputs` slot id copied verbatim from its name; the
937
+ * serializer will not restore it, and the minted id replaces it.
935
938
  *
936
939
  * Everything else stays. A designer document orders stages before its trigger,
937
940
  * carries ids no label seeding reproduces, and needs every one of them.
@@ -943,6 +946,11 @@ function prunePreservation(preservation, plan) {
943
946
  delete identities.id;
944
947
  if (identities.triggers?.length === 1 && identities.triggers[0]?.id === 'trigger_1')
945
948
  delete identities.triggers;
949
+ for (const lane of ['inputs', 'outputs']) {
950
+ const rows = identities.variables?.[lane];
951
+ if (Array.isArray(rows))
952
+ identities.variables[lane] = rows.filter((row) => !isUnrestorableSlotId(lane, row));
953
+ }
946
954
  for (const key of ['triggers', 'stages', 'caseExitRules', 'slaRules'])
947
955
  if (Array.isArray(identities[key]) && identities[key].length === 0)
948
956
  delete identities[key];
@@ -1,5 +1,13 @@
1
1
  import type { BuiltCase } from './case-sdk.js';
2
2
  export type PreservedVariableLane = 'inputs' | 'outputs' | 'inputOutputs';
3
+ /**
4
+ * A preserved `inputs[]` / `outputs[]` slot id copied verbatim from its name is
5
+ * the STRICT_FORMAL_ARG_ID defect, not an identity to keep; the minted id stands.
6
+ */
7
+ export declare function isUnrestorableSlotId(lane: string, row: {
8
+ name: string;
9
+ id?: string;
10
+ }): boolean;
3
11
  /** One platform-owned node retained at its original position in `nodes[]`. */
4
12
  export interface PreservedCaseNode {
5
13
  /** Original zero-based position in the top-level `nodes[]` array. */
@@ -1,3 +1,10 @@
1
+ /**
2
+ * A preserved `inputs[]` / `outputs[]` slot id copied verbatim from its name is
3
+ * the STRICT_FORMAL_ARG_ID defect, not an identity to keep; the minted id stands.
4
+ */
5
+ export function isUnrestorableSlotId(lane, row) {
6
+ return lane !== 'inputOutputs' && typeof row.id === 'string' && row.id === row.name;
7
+ }
1
8
  const preserved = new WeakMap();
2
9
  /** Attach decompiler-owned, non-authoring wire data to a built case. */
3
10
  export function preserveCaseJson(built, payload) {
@@ -9,6 +9,7 @@
9
9
  * name) are resolved here to the generated ids.
10
10
  */
11
11
  import { IdGen } from './ids.js';
12
+ import { isUnrestorableSlotId } from './preserve.js';
12
13
  import { buildConnectorInputs, nestConnectorBody, transportHttpMethod } from '../core/connector-inputs.js';
13
14
  import { EQUALS_EXPR_DIALECT } from '../core/connector-checks.js';
14
15
  import { bindingLabelRefusal } from '../core/binding-messages.js';
@@ -502,7 +503,11 @@ function applyPreservedIdentities(result, preservation) {
502
503
  takeSlas(metadata.slaRules, identities.slaRules);
503
504
  const variables = result.variables;
504
505
  for (const [lane, rows] of Object.entries(identities.variables ?? {}))
505
- (rows ?? []).forEach((row) => take(variables[lane]?.find((candidate) => candidate.name === row.name), row.id));
506
+ (rows ?? []).forEach((row) => {
507
+ if (isUnrestorableSlotId(lane, row))
508
+ return;
509
+ take(variables[lane]?.find((candidate) => candidate.name === row.name), row.id);
510
+ });
506
511
  const replace = (value) => {
507
512
  if (replacements.has(value))
508
513
  return replacements.get(value);
package/dist/check.js CHANGED
@@ -34,9 +34,29 @@ import { plainHttpOnErrorMessage } from './nodes/http/flow/family.js';
34
34
  import { resolveVersion, resolveVersionCarried } from './core/nodes/versioned.js';
35
35
  import { declaredErrorPortRefusal } from './core/error-port.js';
36
36
  import { bodyUsesBreak, loopParallelWarning, loopVersionRefusal } from './loop-version.js';
37
+ import { derivedResourceNodeId } from './core/agent-resource-identity.js';
38
+ const newResourceIdLedger = () => ({ explicit: new Map(), taken: new Map(), agentSources: new Map() });
39
+ /** INLINE_AGENT_RESOURCE_NODE_ID for each explicit `nodeId` another node of the file also gets. */
40
+ function resourceNodeIdClashes(ledger) {
41
+ const out = [];
42
+ for (const [id, { step, what }] of ledger.explicit) {
43
+ const other = ledger.taken.get(id);
44
+ if (other === undefined)
45
+ continue;
46
+ out.push({
47
+ level: 'error', code: 'INLINE_AGENT_RESOURCE_NODE_ID', step,
48
+ message: `Inline agent step "${step}" gives its ${what} the \`nodeId\` "${id}", which is already `
49
+ + `${other}. Node ids are unique in a flow, so compile cannot write both. Give the resource another id, `
50
+ + 'or omit `nodeId` to derive one.',
51
+ });
52
+ }
53
+ return out;
54
+ }
37
55
  export function check(built, opts = {}) {
38
56
  let diags = [];
39
- const uses = collectConnectionUses(() => { diags = checkFlow(built, [], opts); });
57
+ const ledger = newResourceIdLedger();
58
+ const uses = collectConnectionUses(() => { diags = checkFlow(built, [], opts, ledger); });
59
+ diags.push(...resourceNodeIdClashes(ledger));
40
60
  for (const { id, message } of connectionConnectorConflicts(uses, opts)) {
41
61
  diags.push({ level: 'error', code: 'CONNECTION_CONNECTOR_CONFLICT', step: id, message });
42
62
  }
@@ -87,7 +107,7 @@ function checkStepIds(stepNames, trigger, diags) {
87
107
  * to notice a flow that (transitively) calls itself before the serializer
88
108
  * recurses forever.
89
109
  */
90
- function checkFlow(built, ancestors, opts = {}) {
110
+ function checkFlow(built, ancestors, opts = {}, ledger = newResourceIdLedger()) {
91
111
  const diags = [];
92
112
  if (!built.id)
93
113
  diags.push({ level: 'error', code: 'NO_ID', message: 'flow() requires an id.' });
@@ -516,7 +536,7 @@ function checkFlow(built, ancestors, opts = {}) {
516
536
  checkAgent(s.name, s.spec.inputs, diags);
517
537
  }
518
538
  if (s.spec.kind === 'inlineAgent') {
519
- checkInlineAgent(s.name, s.spec.inputs, diags, opts);
539
+ checkInlineAgent(s.name, s.spec.inputs, diags, opts, ledger);
520
540
  }
521
541
  {
522
542
  const rule = OUTPUT_READ_RULES[s.spec.kind];
@@ -878,6 +898,15 @@ function checkFlow(built, ancestors, opts = {}) {
878
898
  }
879
899
  reportUnreachableHitlOutcomes(hitlChoiceNoPorts, exprs, diags);
880
900
  checkRegisteredFamilies(registeredSteps, unresolvedSteps, stepNames, diags);
901
+ // The ids this scope's other nodes get, for the file-wide nodeId check in `check`.
902
+ for (const n of stepNames)
903
+ if (!ledger.taken.has(n))
904
+ ledger.taken.set(n, `the id of step "${n}"`);
905
+ if (!ledger.taken.has(triggerId))
906
+ ledger.taken.set(triggerId, 'the id of the trigger');
907
+ for (const ep of built.entryPoints ?? [])
908
+ if (!ledger.taken.has(ep.id))
909
+ ledger.taken.set(ep.id, `the id of entry point "${ep.id}"`);
881
910
  // Entry points: unique roots with valid triggers, colliding with nothing.
882
911
  const epIds = new Set();
883
912
  for (const ep of built.entryPoints ?? []) {
@@ -1294,7 +1323,7 @@ function checkFlow(built, ancestors, opts = {}) {
1294
1323
  for (const [stepName, child] of subflowChildren) {
1295
1324
  if (ancestors.includes(child) || child === built)
1296
1325
  continue;
1297
- for (const d of checkFlow(child, [...ancestors, built], opts)) {
1326
+ for (const d of checkFlow(child, [...ancestors, built], opts, ledger)) {
1298
1327
  diags.push({
1299
1328
  ...d,
1300
1329
  code: d.code,
@@ -3061,7 +3090,7 @@ export function jsonTextDirective(text, returnKeys) {
3061
3090
  * ALSO asks for JSON text makes the model pack its whole answer, serialized, into
3062
3091
  * one string field. See {@link jsonTextDirective}.
3063
3092
  */
3064
- function checkInlineAgent(step, inputs, diags, opts = {}) {
3093
+ function checkInlineAgent(step, inputs, diags, opts = {}, ledger = newResourceIdLedger()) {
3065
3094
  const at = (code, message, suggestion) => {
3066
3095
  diags.push({ level: 'error', code, message, step, ...(suggestion ? { suggestion } : {}) });
3067
3096
  };
@@ -3168,6 +3197,79 @@ function checkInlineAgent(step, inputs, diags, opts = {}) {
3168
3197
  + `and the one v1's smoke check for this node type FAILS a flow for keeping (it is the definition's `
3169
3198
  + `\`inputDefaults.model\`, not a choice). Pick a current model: \`uip agent model list\`.`);
3170
3199
  }
3200
+ // Where each resource node sits (`nodeId`, `source`): decompile writes them
3201
+ // for a node the canvas placed, and compile writes them as given. A blank or
3202
+ // repeated id would make compile refuse the flow or overwrite another node,
3203
+ // and a `source` that is not a uuid names a directory the platform does not
3204
+ // expect (the agent's own `source` has the same rule).
3205
+ const asList = (v) => (v === undefined ? [] : Array.isArray(v) ? v : [v]);
3206
+ const placed = [
3207
+ ...asList(inputs?.context).map((r) => ({ what: `context index "${r?.name ?? ''}"`, kind: 'Context', nodeId: r?.nodeId, source: r?.source })),
3208
+ ...asList(inputs?.tools).map((r) => ({ what: `${r?.kind ?? ''} tool`, kind: 'Tool', nodeId: r?.nodeId, source: r?.source })),
3209
+ ...asList(inputs?.escalation).map((r) => ({ what: `escalation "${r?.name ?? ''}"`, kind: 'Escalation', nodeId: r?.nodeId, source: r?.source })),
3210
+ ];
3211
+ // The ids compile derives for the resources with no `nodeId`: the n-th one of
3212
+ // a kind gets `<step><Kind>`, then `<step><Kind>2`, ... An explicit id equal to
3213
+ // one of them makes compile refuse the flow, or rename the derived node.
3214
+ for (const kind of ['Context', 'Tool', 'Escalation']) {
3215
+ const derived = placed.filter((r) => r.kind === kind && r.nodeId === undefined);
3216
+ derived.forEach((r, n) => {
3217
+ const id = derivedResourceNodeId(step, kind, n);
3218
+ if (!ledger.taken.has(id))
3219
+ ledger.taken.set(id, `the id compile derives for the ${r.what} of inline agent step "${step}"`);
3220
+ });
3221
+ }
3222
+ /** Explicit `source` -> the resource on THIS agent that has it: two would write one resource.json. */
3223
+ const sources = new Map();
3224
+ for (const r of placed) {
3225
+ if (r.nodeId !== undefined) {
3226
+ const id = typeof r.nodeId === 'string' ? r.nodeId : '';
3227
+ const other = ledger.explicit.get(id);
3228
+ if (id.trim() === '') {
3229
+ at('INLINE_AGENT_RESOURCE_NODE_ID', `Inline agent step "${step}" gives its ${r.what} the \`nodeId\` ${JSON.stringify(r.nodeId)}, which is `
3230
+ + `not a node id. Omit \`nodeId\` to derive one, or give the id the node has in the flow.`);
3231
+ }
3232
+ else if (other !== undefined) {
3233
+ at('INLINE_AGENT_RESOURCE_NODE_ID', `Inline agent step "${step}" gives its ${r.what} the \`nodeId\` "${id}", which the ${other.what} `
3234
+ + `(on step "${other.step}") already has. Node ids are unique in a flow.`);
3235
+ }
3236
+ else {
3237
+ ledger.explicit.set(id, { step, what: r.what });
3238
+ }
3239
+ }
3240
+ if (r.source === undefined)
3241
+ continue;
3242
+ // Not trimmed: compile writes `source` as given, into `inputs.source` and the directory name.
3243
+ if (!GUID_RE.test(String(r.source))) {
3244
+ at('INLINE_AGENT_RESOURCE_SOURCE_NOT_GUID', `Inline agent step "${step}" gives its ${r.what} the \`source\` ${JSON.stringify(r.source)}, which is not `
3245
+ + `a uuid${/^\s|\s$/.test(String(r.source)) ? ' (it has spaces around it)' : ''}. \`source\` is the resource's `
3246
+ + `directory under the agent's own (\`resources/<source>/resource.json\`) and the node's \`inputs.source\`. `
3247
+ + 'Omit it and one is derived from the flow id, the step and the resource.');
3248
+ continue;
3249
+ }
3250
+ const key = String(r.source).toLowerCase();
3251
+ const first = sources.get(key);
3252
+ if (first !== undefined) {
3253
+ at('INLINE_AGENT_RESOURCE_SOURCE_DUP', `Inline agent step "${step}" gives its ${first} and its ${r.what} the same \`source\` "${String(r.source)}". `
3254
+ + 'Both would write `resources/<source>/resource.json` under this agent, and the second file would replace '
3255
+ + 'the first. Give each resource its own `source`, or omit `source` to derive one.');
3256
+ }
3257
+ else {
3258
+ sources.set(key, r.what);
3259
+ }
3260
+ }
3261
+ if (typeof inputs?.source === 'string' && GUID_RE.test(inputs.source.trim())) {
3262
+ const key = inputs.source.trim().toLowerCase();
3263
+ const other = ledger.agentSources.get(key);
3264
+ if (other !== undefined && other !== step) {
3265
+ at('INLINE_AGENT_RESOURCE_SOURCE_DUP', `Inline agent steps "${other}" and "${step}" have the same \`source\` "${inputs.source}". Both would write `
3266
+ + '`<source>/agent.json`, and the second file would replace the first. Give each agent its own `source`, '
3267
+ + 'or omit it to derive one.');
3268
+ }
3269
+ else {
3270
+ ledger.agentSources.set(key, step);
3271
+ }
3272
+ }
3171
3273
  if (inputs?.source !== undefined && !GUID_RE.test(String(inputs.source).trim())) {
3172
3274
  at('INLINE_AGENT_SOURCE_NOT_GUID', `Inline agent step "${step}"'s \`source\` is ${JSON.stringify(inputs.source)}, which is not a uuid. `
3173
3275
  + `\`source\` is the agent's own DIRECTORY name, and the platform resolves its configuration at `
@@ -1131,7 +1131,43 @@ export type BuiltinToolName = 'analyzefiles' | 'summarize' | 'batchtransform';
1131
1131
  * `tool.mcp.*`/`uipath.agent.resource.mcp.*` resolves to nothing on this tenant
1132
1132
  * (no MCP server exists to point at, so there is no definition to derive).
1133
1133
  */
1134
- export type ToolRef = BuiltinToolRef | ConnectorToolRef | ProcessToolRef | IxpToolRef | McpToolRef | RemoteA2aToolRef | ClientSideToolRef | HttpRequestToolRef;
1134
+ export type ToolRef = (BuiltinToolRef | ConnectorToolRef | ProcessToolRef | IxpToolRef | McpToolRef | RemoteA2aToolRef | ClientSideToolRef | HttpRequestToolRef) & AgentResourcePlacement;
1135
+ /**
1136
+ * Where one of an inline agent's resources (a context index, a tool, an
1137
+ * escalation) sits in the `.flow`: the id of its own resource NODE and the
1138
+ * directory that holds its `resource.json`.
1139
+ *
1140
+ * @remarks
1141
+ * Omit both when you write a flow: compile derives them, so a recompile is not
1142
+ * a diff. Decompile writes them only for a file whose resource node another
1143
+ * tool placed (the canvas, `uip maestro flow node add`) under an id or directory
1144
+ * compile would not derive, so a no-edit round trip keeps the node, its layout
1145
+ * entry and its directory instead of replacing them.
1146
+ *
1147
+ * ```ts
1148
+ * // decompiled from a canvas file that named the index's node `ctx_7f2a1b`
1149
+ * context: [{
1150
+ * name: 'Example SOP Index', id: '11111111-2222-4333-8444-555555555555',
1151
+ * nodeId: 'ctx_7f2a1b', source: 'd84e2c17-6a3f-4c59-b0e2-7f1a9c5b3d28',
1152
+ * }]
1153
+ * ```
1154
+ */
1155
+ export interface AgentResourcePlacement {
1156
+ /**
1157
+ * The resource node's id. Defaults to `<agent step>Context`, `<agent step>Tool`
1158
+ * or `<agent step>Escalation`, numbered from 2 for a second resource of the
1159
+ * same kind (`<agent step>Context2`). Compile writes it as given and refuses an
1160
+ * id another node in the flow already has (`check`: `INLINE_AGENT_RESOURCE_NODE_ID`).
1161
+ */
1162
+ nodeId?: string;
1163
+ /**
1164
+ * The resource's directory id, a uuid: the node's `inputs.source` and the
1165
+ * `id` of `<agent source>/resources/<source>/resource.json`. Defaults to one
1166
+ * derived from the flow id, the agent step and the resource, so a given flow
1167
+ * emits the same uuid every time.
1168
+ */
1169
+ source?: string;
1170
+ }
1135
1171
  /**
1136
1172
  * An MCP server the agent may call tools on. The node type is minted from the
1137
1173
  * server's name and key exactly as the platform's own mapper does
@@ -1398,9 +1434,9 @@ export interface IxpToolRef {
1398
1434
  /**
1399
1435
  * One escalation an inline agent may raise — an Action Center app task a human
1400
1436
  * completes (the `coded-action-app` variant, the one this tenant's registry
1401
- * serves).
1437
+ * serves), placed in the flow by {@link AgentResourcePlacement}.
1402
1438
  */
1403
- export interface EscalationRef {
1439
+ export interface EscalationRef extends AgentResourcePlacement {
1404
1440
  /** The escalation's name — required by the definition (`minLength: 1`). */
1405
1441
  name: string;
1406
1442
  /** When the agent should escalate — what a reviewer sees as the reason. */
@@ -1463,13 +1499,14 @@ export interface EscalationRef {
1463
1499
  labels?: string[];
1464
1500
  }
1465
1501
  /**
1466
- * One Context Grounding index an inline agent is grounded on.
1502
+ * One Context Grounding index an inline agent is grounded on, placed in the
1503
+ * flow by {@link AgentResourcePlacement}.
1467
1504
  *
1468
1505
  * @remarks
1469
1506
  * The optional fields mirror the platform's own `inputDefaults` for an index
1470
1507
  * resource, so omitting them emits exactly what the designer would.
1471
1508
  */
1472
- export interface ContextIndexRef {
1509
+ export interface ContextIndexRef extends AgentResourcePlacement {
1473
1510
  /** The index's display name on the tenant, e.g. `'Billing Dispute SOP Index'`. */
1474
1511
  name: string;
1475
1512
  /**
@@ -1496,6 +1533,11 @@ export interface ContextIndexRef {
1496
1533
  threshold?: number;
1497
1534
  /** Restrict to one file type; `'All'` (the platform's default) or e.g. `'pdf'`. */
1498
1535
  fileExtension?: string;
1536
+ /**
1537
+ * The resource node's label on the canvas. Defaults to `name`. Only the
1538
+ * canvas shows it; the index is found by `name` and `id`.
1539
+ */
1540
+ label?: string;
1499
1541
  }
1500
1542
  /**
1501
1543
  * Every action kind, keyed by kind, to the body of its spec — the arm's fields
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The identity compile derives for an inline agent's resource node when the
3
+ * author gives none: its node id and its resource directory (`inputs.source`).
4
+ *
5
+ * Serialize uses these to name what it emits. Decompile uses the same functions
6
+ * to decide whether a file's resource node carries the identity compile would
7
+ * derive anyway; when it does not (the canvas or the CLI placed the node), the
8
+ * decompiled source spells it out as `nodeId` / `source`
9
+ * (`AgentResourcePlacement`). One definition for both directions keeps
10
+ * them inverses: a second copy in decompile would drift from serialize's.
11
+ */
12
+ import type { ToolRef } from './actions.js';
13
+ /** The resource handles whose nodes compile names after the agent step. */
14
+ export type AgentResourceKind = 'Context' | 'Tool' | 'Escalation' | 'Memory';
15
+ /**
16
+ * The node id compile gives the `n`-th (0-based) resource of `kind` on agent
17
+ * step `agentStep` when no `nodeId` is set: `<step><Kind>`, then
18
+ * `<step><Kind>2`, `<step><Kind>3`, ... Characters a node id cannot hold become
19
+ * `_`, as compile's own id claim does. This assumes no other node already holds
20
+ * that id, which is the case for a flow compile wrote.
21
+ */
22
+ export declare function derivedResourceNodeId(agentStep: string, kind: AgentResourceKind, n: number): string;
23
+ /** The directory compile derives for a context index (`id` = the index id). */
24
+ export declare function derivedContextSource(flowId: string, agentStep: string, indexId: string): string;
25
+ /**
26
+ * What tells one tool on an agent from another for its derived directory: the
27
+ * built-in's name, the connector operation, the IxP project, the client-side or
28
+ * HTTP tool's name, else the resource key.
29
+ */
30
+ export declare function toolResourceSeed(tr: ToolRef): string;
31
+ /** The directory compile derives for a tool. */
32
+ export declare function derivedToolSource(flowId: string, agentStep: string, tr: ToolRef): string;
33
+ /** The directory compile derives for an escalation (`name` = the escalation's name). */
34
+ export declare function derivedEscalationSource(flowId: string, agentStep: string, name: string): string;
@@ -0,0 +1,37 @@
1
+ import { fnv1aX4Uuid } from './stable-id.js';
2
+ /**
3
+ * The node id compile gives the `n`-th (0-based) resource of `kind` on agent
4
+ * step `agentStep` when no `nodeId` is set: `<step><Kind>`, then
5
+ * `<step><Kind>2`, `<step><Kind>3`, ... Characters a node id cannot hold become
6
+ * `_`, as compile's own id claim does. This assumes no other node already holds
7
+ * that id, which is the case for a flow compile wrote.
8
+ */
9
+ export function derivedResourceNodeId(agentStep, kind, n) {
10
+ const base = `${agentStep}${kind}`.replace(/[^A-Za-z0-9_]/g, '_');
11
+ return n === 0 ? base : `${base}${n + 1}`;
12
+ }
13
+ /** The directory compile derives for a context index (`id` = the index id). */
14
+ export function derivedContextSource(flowId, agentStep, indexId) {
15
+ return fnv1aX4Uuid(`${flowId}/${agentStep}/context/${indexId}`);
16
+ }
17
+ /**
18
+ * What tells one tool on an agent from another for its derived directory: the
19
+ * built-in's name, the connector operation, the IxP project, the client-side or
20
+ * HTTP tool's name, else the resource key.
21
+ */
22
+ export function toolResourceSeed(tr) {
23
+ return tr.kind === 'builtin' ? tr.tool
24
+ : tr.kind === 'connector' ? `${tr.connector}.${tr.operation}`
25
+ : tr.kind === 'ixp' ? tr.projectId
26
+ : tr.kind === 'clientside' ? `clientside.${tr.name}`
27
+ : tr.kind === 'httpRequest' ? `httprequest.${tr.name ?? 'default'}`
28
+ : tr.key;
29
+ }
30
+ /** The directory compile derives for a tool. */
31
+ export function derivedToolSource(flowId, agentStep, tr) {
32
+ return fnv1aX4Uuid(`${flowId}/${agentStep}/tool/${toolResourceSeed(tr)}`);
33
+ }
34
+ /** The directory compile derives for an escalation (`name` = the escalation's name). */
35
+ export function derivedEscalationSource(flowId, agentStep, name) {
36
+ return fnv1aX4Uuid(`${flowId}/${agentStep}/escalation/${name}`);
37
+ }
package/dist/decompile.js CHANGED
@@ -38,6 +38,7 @@ import { commentText } from './core/comment-text.js';
38
38
  import { tsLiteral, tsNumber, tsText } from './core/ts-literal.js';
39
39
  import { ownEntry } from './core/own-entry.js';
40
40
  import { CONTEXT_INDEX_DEFAULTS, CONTEXT_INDEX_DEFAULT_FILE_EXTENSION, contextIndexSlug, parseContextIndexType, } from './core/context-index.js';
41
+ import { derivedContextSource, derivedEscalationSource, derivedResourceNodeId, derivedToolSource, } from './core/agent-resource-identity.js';
41
42
  import { wireExprBody } from './core/wire-expr.js';
42
43
  import { hasExits } from './core/step-ports.js';
43
44
  import { parseIxpProjectType } from './core/actions.js';
@@ -107,6 +108,12 @@ let runWarned = new Set();
107
108
  * `.layout()` key, must use the id compile will produce, not the file's.
108
109
  */
109
110
  let runAbsorbed = new Map();
111
+ /**
112
+ * The id the recompiled flow will have: the `flow(...)` argument decompile
113
+ * writes. Compile derives a resource's directory from it, so decompile needs it
114
+ * to tell a derived directory from one the file chose.
115
+ */
116
+ let runFlowId = 'flow';
110
117
  /** `DecompileOptions.readSidecar` for the current run. */
111
118
  let runReadSidecar;
112
119
  function warnOnce(message) {
@@ -1160,20 +1167,45 @@ function emitAgentCluster(node, imp, inputNames, graph) {
1160
1167
  // spec order is this edge order: the first is `<agent><Kind>`, the n-th
1161
1168
  // `<agent><Kind><n>` (claimId's dedupe). Only the first memory is recovered,
1162
1169
  // so every further memory maps to `undefined`.
1170
+ //
1171
+ // When a port's nodes do not ALL carry those ids (the canvas or the CLI named
1172
+ // them), every resource on the port is written with its own `nodeId`, and
1173
+ // compile keeps the file's ids. All of them, not only the odd one: compile
1174
+ // numbers the derived ids in order, so a derived id after an explicit one
1175
+ // would come out one lower than the file's.
1163
1176
  // This reads the edges directly, not through `resource()`: it must see EVERY
1164
1177
  // artifact edge, including ones the spec does not carry, and it must not
1165
1178
  // account for any edge.
1179
+ const keepIds = new Set();
1166
1180
  for (const [port, kind, emittedCount] of [
1167
1181
  ['context', 'Context', Infinity], ['tool', 'Tool', Infinity],
1168
1182
  ['escalation', 'Escalation', Infinity], ['memory', 'Memory', 1],
1169
1183
  ]) {
1170
- const base = `${node.id}${kind}`.replace(/[^A-Za-z0-9_]/g, '_');
1171
- (graph?.outEdges(node.id) ?? [])
1184
+ const targets = (graph?.outEdges(node.id) ?? [])
1172
1185
  .filter((e) => e.sourcePort === port && graph.byId.has(e.targetNodeId))
1173
- .forEach((e, k) => {
1174
- runAbsorbed.set(e.targetNodeId, k >= emittedCount ? undefined : k === 0 ? base : `${base}${k + 1}`);
1186
+ .map((e) => e.targetNodeId);
1187
+ const keep = port !== 'memory'
1188
+ && targets.some((id, k) => id !== derivedResourceNodeId(node.id, kind, k));
1189
+ targets.forEach((id, k) => {
1190
+ if (keep)
1191
+ keepIds.add(id);
1192
+ runAbsorbed.set(id, keep ? id : k >= emittedCount ? undefined : derivedResourceNodeId(node.id, kind, k));
1175
1193
  });
1176
1194
  }
1195
+ /**
1196
+ * The `nodeId` / `source` entries for resource node `r`: each only when it
1197
+ * differs from what compile derives (`derivedSource`), so a flow compile
1198
+ * wrote decompiles without them.
1199
+ */
1200
+ const placement = (r, derivedSource) => {
1201
+ const source = unwrapSource((r.inputs ?? {}).source);
1202
+ return [
1203
+ ['nodeId', keepIds.has(r.id) ? tsLiteral(r.id) : undefined],
1204
+ ['source', source !== undefined && source !== '' && source !== derivedSource ? tsLiteral(source) : undefined],
1205
+ ];
1206
+ };
1207
+ /** A tool's options plus its placement; `seed` is the ref compile derives the tool's directory from. */
1208
+ const toolOpts = (t, seed, entries) => opts([...entries, ...placement(t, derivedToolSource(runFlowId, node.id, seed))]);
1177
1209
  const rInputs = (n) => (n.inputs ?? {});
1178
1210
  const rPlain = (n, k) => {
1179
1211
  const raw = unwrapSource(rInputs(n)[k]);
@@ -1189,7 +1221,7 @@ function emitAgentCluster(node, imp, inputNames, graph) {
1189
1221
  return undefined;
1190
1222
  return objLit(Object.entries(props).map(([k, x]) => [k, tsLiteral(String(x?.type ?? 'string'))]));
1191
1223
  };
1192
- return opts([
1224
+ return toolOpts(t, { kind: 'clientside', name: rPlain(t, 'name') ?? '' }, [
1193
1225
  ['kind', tsLiteral('clientside')],
1194
1226
  ['name', tsLiteral(rPlain(t, 'name') ?? '')],
1195
1227
  ['description', rPlain(t, 'description') !== undefined ? tsLiteral(rPlain(t, 'description')) : undefined],
@@ -1206,7 +1238,8 @@ function emitAgentCluster(node, imp, inputNames, graph) {
1206
1238
  const v = row.textValue;
1207
1239
  return typeof v === 'number' ? String(v) : typeof v === 'string' ? tsLiteral(v) : JSON.stringify(v);
1208
1240
  };
1209
- return opts([
1241
+ // `name` is not recovered, so compile derives the directory from the default.
1242
+ return toolOpts(t, { kind: 'httpRequest' }, [
1210
1243
  ['kind', tsLiteral('httpRequest')],
1211
1244
  ['url', fixed('url')], ['method', fixed('method')],
1212
1245
  ['headers', fixed('headers')], ['params', fixed('params')],
@@ -1214,7 +1247,7 @@ function emitAgentCluster(node, imp, inputNames, graph) {
1214
1247
  ]);
1215
1248
  }
1216
1249
  if (type.startsWith('uipath.agent.resource.tool.mcp.')) {
1217
- return opts([
1250
+ return toolOpts(t, { kind: 'mcp', name: '', key: rPlain(t, 'referenceKey') ?? '' }, [
1218
1251
  ['kind', tsLiteral('mcp')],
1219
1252
  ['name', tsLiteral(rPlain(t, 'name') ?? '')],
1220
1253
  ['key', tsLiteral(rPlain(t, 'referenceKey') ?? '')],
@@ -1225,7 +1258,7 @@ function emitAgentCluster(node, imp, inputNames, graph) {
1225
1258
  ]);
1226
1259
  }
1227
1260
  if (type.startsWith('uipath.agent.resource.tool.a2a.')) {
1228
- return opts([
1261
+ return toolOpts(t, { kind: 'a2a', name: '', slug: '', key: rPlain(t, 'referenceKey') ?? '' }, [
1229
1262
  ['kind', tsLiteral('a2a')],
1230
1263
  ['name', tsLiteral(rPlain(t, 'name') ?? '')],
1231
1264
  ['key', tsLiteral(rPlain(t, 'referenceKey') ?? '')],
@@ -1234,17 +1267,19 @@ function emitAgentCluster(node, imp, inputNames, graph) {
1234
1267
  ]);
1235
1268
  }
1236
1269
  if (type.startsWith('uipath.agent.resource.tool.builtin.')) {
1237
- return opts([
1270
+ const tool = type.slice('uipath.agent.resource.tool.builtin.'.length);
1271
+ return toolOpts(t, { kind: 'builtin', tool }, [
1238
1272
  ['kind', tsLiteral('builtin')],
1239
- ['tool', tsLiteral(type.slice('uipath.agent.resource.tool.builtin.'.length))],
1273
+ ['tool', tsLiteral(tool)],
1240
1274
  ['description', rPlain(t, 'description') !== undefined ? tsLiteral(rPlain(t, 'description')) : undefined],
1241
1275
  ]);
1242
1276
  }
1243
1277
  if (type.startsWith('uipath.agent.resource.tool.ixp.')) {
1244
1278
  const tail = type.slice('uipath.agent.resource.tool.ixp.'.length);
1245
- return opts([
1279
+ const projectId = tail.slice(tail.indexOf('.') + 1);
1280
+ return toolOpts(t, { kind: 'ixp', name: '', projectId }, [
1246
1281
  ['kind', tsLiteral('ixp')],
1247
- ['projectId', tsLiteral(tail.slice(tail.indexOf('.') + 1))],
1282
+ ['projectId', tsLiteral(projectId)],
1248
1283
  ['name', tsLiteral(rPlain(t, 'name') ?? '')],
1249
1284
  ]);
1250
1285
  }
@@ -1266,9 +1301,10 @@ function emitAgentCluster(node, imp, inputNames, graph) {
1266
1301
  const row = values.find((v) => v.name === n);
1267
1302
  return typeof row?.default === 'string' ? row.default : undefined;
1268
1303
  };
1269
- return opts([
1304
+ const key = type.slice(ns[0].length);
1305
+ return toolOpts(t, { kind: 'process', name: '', folderPath: '', key }, [
1270
1306
  ['kind', tsLiteral(ns[1])],
1271
- ['key', tsLiteral(type.slice(ns[0].length))],
1307
+ ['key', tsLiteral(key)],
1272
1308
  ['name', tsLiteral(bound('name') ?? rPlain(t, 'name') ?? '')],
1273
1309
  ['folderPath', tsLiteral(bound('folderPath') ?? '')],
1274
1310
  ]);
@@ -1277,6 +1313,9 @@ function emitAgentCluster(node, imp, inputNames, graph) {
1277
1313
  });
1278
1314
  const contextSrc = resource('context').map((c) => {
1279
1315
  const { name, id, folderPath, sidecar } = contextIndexIdentity(node, c, rPlain);
1316
+ // Compile labels the node with `name`; a canvas rename is kept as `label`.
1317
+ const label = typeof c.display?.label === 'string' && c.display.label !== '' && c.display.label !== name
1318
+ ? c.display.label : undefined;
1280
1319
  return opts([
1281
1320
  ['name', tsLiteral(name)],
1282
1321
  ['id', tsLiteral(id)],
@@ -1284,6 +1323,8 @@ function emitAgentCluster(node, imp, inputNames, graph) {
1284
1323
  ['folderKey', rPlain(c, 'folderKey') !== undefined ? tsLiteral(rPlain(c, 'folderKey')) : undefined],
1285
1324
  // `query` and the retrieval settings, from resource.json or the node (#1183).
1286
1325
  ...contextIndexSettings(node, c, sidecar),
1326
+ ['label', label !== undefined ? tsLiteral(label) : undefined],
1327
+ ...placement(c, derivedContextSource(runFlowId, node.id, id)),
1287
1328
  ]);
1288
1329
  });
1289
1330
  const escalationSrc = resource('escalation').map((e) => {
@@ -1323,6 +1364,7 @@ function emitAgentCluster(node, imp, inputNames, graph) {
1323
1364
  ['taskTitle', typeof extra.taskTitle === 'string' && extra.taskTitle !== '' ? tsLiteral(extra.taskTitle) : undefined],
1324
1365
  // 'medium' is the platform default.
1325
1366
  ['priority', typeof extra.priority === 'string' && extra.priority !== 'medium' ? tsLiteral(extra.priority) : undefined],
1367
+ ...placement(e, derivedEscalationSource(runFlowId, node.id, rPlain(e, 'name') ?? '')),
1326
1368
  ]);
1327
1369
  });
1328
1370
  const mem = resource('memory', 1)[0];
@@ -3249,6 +3291,7 @@ export function decompile(flow, options = {}) {
3249
3291
  runDefs = new Map();
3250
3292
  runRawManifests = [];
3251
3293
  runAbsorbed = new Map();
3294
+ runFlowId = slugId(flow);
3252
3295
  for (const d of (flow.definitions ?? [])) {
3253
3296
  if (typeof d?.nodeType !== 'string')
3254
3297
  continue;