@uipath/maestro-builder-sdk 6.32.8 → 6.33.1

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.
@@ -1010,10 +1010,16 @@ declare class StageBuilder {
1010
1010
  * scheduler rules whether they share a set or not, and the designer writes
1011
1011
  * one task per set even for a stage its own SDD calls parallel.
1012
1012
  *
1013
+ * A task without a `runs-sequentially` rule is always written to a set of its
1014
+ * own, placed right after the set `lane` names; `lane` only groups sequential
1015
+ * tasks. Case schema V31 made set membership behavioural: its V30→V31
1016
+ * migration rewrites the entry conditions of any non-sequential task that
1017
+ * shares a set (builder-sdk#1248).
1018
+ *
1013
1019
  * @param displayName - The task's display name.
1014
1020
  * @param fn - Receives a sub-builder for what the task does.
1015
- * @param opts - `lane` is the task's set index in `data.tasks[][]` (default 0);
1016
- * it orders `runs-sequentially` sets and is inert for every other task.
1021
+ * @param opts - `lane` is the index of the set a `runs-sequentially` task joins
1022
+ * in `data.tasks[][]` (default 0). Any other task gets its own set after it.
1017
1023
  * @returns This builder, so calls chain.
1018
1024
  */
1019
1025
  task(displayName: string, fn: (t: TaskBuilder) => void, opts?: {
@@ -516,10 +516,16 @@ class StageBuilder {
516
516
  * scheduler rules whether they share a set or not, and the designer writes
517
517
  * one task per set even for a stage its own SDD calls parallel.
518
518
  *
519
+ * A task without a `runs-sequentially` rule is always written to a set of its
520
+ * own, placed right after the set `lane` names; `lane` only groups sequential
521
+ * tasks. Case schema V31 made set membership behavioural: its V30→V31
522
+ * migration rewrites the entry conditions of any non-sequential task that
523
+ * shares a set (builder-sdk#1248).
524
+ *
519
525
  * @param displayName - The task's display name.
520
526
  * @param fn - Receives a sub-builder for what the task does.
521
- * @param opts - `lane` is the task's set index in `data.tasks[][]` (default 0);
522
- * it orders `runs-sequentially` sets and is inert for every other task.
527
+ * @param opts - `lane` is the index of the set a `runs-sequentially` task joins
528
+ * in `data.tasks[][]` (default 0). Any other task gets its own set after it.
523
529
  * @returns This builder, so calls chain.
524
530
  */
525
531
  task(displayName, fn, opts = {}) {
@@ -10,6 +10,7 @@
10
10
  */
11
11
  import { IdGen } from './ids.js';
12
12
  import { isUnrestorableSlotId } from './preserve.js';
13
+ import { v31TaskSets } from './task-sets.js';
13
14
  import { buildConnectorInputs, nestConnectorBody, transportHttpMethod } from '../core/connector-inputs.js';
14
15
  import { EQUALS_EXPR_DIALECT } from '../core/connector-checks.js';
15
16
  import { bindingLabelRefusal } from '../core/binding-messages.js';
@@ -109,6 +110,8 @@ export function serialize(built, opts = {}) {
109
110
  built.stages.forEach((stage) => prealloc(stage.slaRules, stage.label));
110
111
  const stageIds = [];
111
112
  const taskIds = []; // [stageIndex][lane][taskIndex]
113
+ // The task sets as written: one set per parallel task (see `laneSets`).
114
+ const stageLanes = built.stages.map((stage) => laneSets(stage.lanes));
112
115
  // Task displayNames must be unique across the whole case: the platform rejects
113
116
  // duplicates (CASE_MGMT_STAGE_TASK_NAME_DUPLICATE, CRITICAL), and a bare
114
117
  // `selected-tasks-completed` reference resolves against a single name→id map, so
@@ -121,7 +124,7 @@ export function serialize(built, opts = {}) {
121
124
  resolver.stageIdByLabel.set(stage.label, sid);
122
125
  stageIds.push(sid);
123
126
  taskIds.push([]);
124
- stage.lanes.forEach((lane, li) => {
127
+ stageLanes[si].forEach((lane, li) => {
125
128
  taskIds[si].push([]);
126
129
  lane.forEach((task) => {
127
130
  if (seenTaskNames.has(task.name))
@@ -144,7 +147,7 @@ export function serialize(built, opts = {}) {
144
147
  const triggerNodeByRef = new Map();
145
148
  built.triggers.forEach((t, i) => triggerNodeByRef.set(t, triggerNodes[i]));
146
149
  built.stages.forEach((stage, si) => {
147
- nodes.push(emitStage(stage, si, stageIds[si], taskIds[si], ids, resolver));
150
+ nodes.push(emitStage(stage, stageLanes[si], si, stageIds[si], taskIds[si], ids, resolver));
148
151
  });
149
152
  // ── Metadata (case-level) ──
150
153
  const metadata = {
@@ -382,7 +385,7 @@ export function applyPreservation(result, preservation, options = {}) {
382
385
  .filter((node) => String(node.type ?? '').endsWith('Stage'));
383
386
  for (const entry of preservation.taskFields ?? []) {
384
387
  const stage = stageNodes.find((candidate) => candidate.data?.label === entry.stage);
385
- const task = stage?.data?.tasks?.[entry.lane]?.find((candidate) => candidate.displayName === entry.task);
388
+ const task = stageTask(stage, entry.task);
386
389
  // A brownfield edit may deliberately delete a typed task or its stage. Its
387
390
  // attached designer-only fields leave with that construct; surviving tasks
388
391
  // still collide-refuse below instead of letting preservation overwrite
@@ -408,7 +411,7 @@ export function applyPreservation(result, preservation, options = {}) {
408
411
  }
409
412
  for (const entry of preservation.omitTaskElementIds ?? []) {
410
413
  const stage = stageNodes.find((candidate) => candidate.data?.label === entry.stage);
411
- const task = stage?.data?.tasks?.[entry.lane]?.find((candidate) => candidate.displayName === entry.task);
414
+ const task = stageTask(stage, entry.task);
412
415
  if (task)
413
416
  delete task.elementId;
414
417
  }
@@ -483,8 +486,8 @@ function applyPreservedIdentities(result, preservation) {
483
486
  takeConditions(stage?.data?.entryConditions, stageIdentity.entryConditions);
484
487
  takeConditions(stage?.data?.exitConditions, stageIdentity.exitConditions);
485
488
  takeSlas(stage?.data?.slaRules, stageIdentity.slaRules);
486
- (stageIdentity.tasks ?? []).forEach((lane, laneIndex) => lane.forEach((taskIdentity) => {
487
- const task = stage?.data?.tasks?.[laneIndex]?.find((candidate) => candidate.displayName === taskIdentity.name);
489
+ (stageIdentity.tasks ?? []).forEach((lane) => lane.forEach((taskIdentity) => {
490
+ const task = stageTask(stage, taskIdentity.name);
488
491
  take(task, taskIdentity.id);
489
492
  takeConditions(task?.entryConditions, taskIdentity.entryConditions);
490
493
  (taskIdentity.inputs ?? []).forEach((row, index) => take(task?.data?.inputs?.[index], row.id));
@@ -729,13 +732,25 @@ function emitTriggers(triggers, ids, resolver) {
729
732
  };
730
733
  });
731
734
  }
732
- function emitStage(stage, si, stageId, laneTaskIds, ids, resolver) {
735
+ /** A stage's task sets as the serializer writes them — one set per parallel task (`v31TaskSets`). */
736
+ function laneSets(lanes) {
737
+ return v31TaskSets(lanes, (task) => task.entry.some((condition) => condition.rules.some((group) => group.some((r) => r.rule === 'runs-sequentially'))));
738
+ }
739
+ /** One emitted task of a stage node, by its (case-unique) display name, in any set. */
740
+ function stageTask(stage, name) {
741
+ for (const lane of stage?.data?.tasks ?? [])
742
+ for (const candidate of lane)
743
+ if (candidate.displayName === name)
744
+ return candidate;
745
+ return undefined;
746
+ }
747
+ function emitStage(stage, lanes, si, stageId, laneTaskIds, ids, resolver) {
733
748
  const data = {
734
749
  [CASE_FORMAT_PROFILE.stageLabelField]: stage.label,
735
750
  parentElement: { id: 'root', type: 'case-management:root' },
736
751
  isInvalidDropTarget: false,
737
752
  isPendingParent: false,
738
- tasks: stage.lanes.map((lane, li) => lane.map((task, ti) => emitTask(task, stage.label, laneTaskIds[li][ti], ids, resolver))),
753
+ tasks: lanes.map((lane, li) => lane.map((task, ti) => emitTask(task, stage.label, laneTaskIds[li][ti], ids, resolver))),
739
754
  };
740
755
  if (stage.description)
741
756
  data.description = stage.description;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Task sets (`data.tasks[i]`) in the shape Case schema V31+ keeps unchanged.
3
+ *
4
+ * V31 made a set a sequential chain. Its V30→V31 migration — which the product
5
+ * runs on every V30 document it reads — keeps a set unchanged only when it holds
6
+ * one task, or when every task in it declares a `runs-sequentially` rule. Every
7
+ * other task in a shared set has its entry conditions REPLACED by a generated
8
+ * `runs-sequentially` condition, so two parallel `current-stage-entered` tasks
9
+ * written into one set ran one after the other, under conditions the author
10
+ * never wrote, and nothing reported it (builder-sdk#1248).
11
+ *
12
+ * At V30 the regrouping is free: the converter reads a set's position only for
13
+ * tasks whose every rule is `runs-sequentially`; every other task runs on its
14
+ * own entry condition wherever it sits. A task the migration would rewrite
15
+ * therefore moves to a set of its own right after the set it was written in, so
16
+ * the stage's flattened task order — the order the converter writes its rules
17
+ * in — is unchanged too. Tasks the migration treats as sequential keep their
18
+ * set. The only case that reorders is a set that mixed both kinds: its
19
+ * sequential tasks stay together and the others follow.
20
+ *
21
+ * Generic over the task shape so the serializer (built tasks) and the tests
22
+ * (emitted JSON) apply the one rule.
23
+ */
24
+ export declare function v31TaskSets<T>(sets: readonly (readonly T[])[], runsSequentially: (task: T) => boolean): T[][];
25
+ /** The V30→V31 migration's own test, on emitted task JSON: any rule of any entry condition is `runs-sequentially`. */
26
+ export declare function taskJsonRunsSequentially(task: {
27
+ entryConditions?: Array<{
28
+ rules?: Array<Array<{
29
+ rule?: string;
30
+ }>>;
31
+ }>;
32
+ }): boolean;
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Task sets (`data.tasks[i]`) in the shape Case schema V31+ keeps unchanged.
3
+ *
4
+ * V31 made a set a sequential chain. Its V30→V31 migration — which the product
5
+ * runs on every V30 document it reads — keeps a set unchanged only when it holds
6
+ * one task, or when every task in it declares a `runs-sequentially` rule. Every
7
+ * other task in a shared set has its entry conditions REPLACED by a generated
8
+ * `runs-sequentially` condition, so two parallel `current-stage-entered` tasks
9
+ * written into one set ran one after the other, under conditions the author
10
+ * never wrote, and nothing reported it (builder-sdk#1248).
11
+ *
12
+ * At V30 the regrouping is free: the converter reads a set's position only for
13
+ * tasks whose every rule is `runs-sequentially`; every other task runs on its
14
+ * own entry condition wherever it sits. A task the migration would rewrite
15
+ * therefore moves to a set of its own right after the set it was written in, so
16
+ * the stage's flattened task order — the order the converter writes its rules
17
+ * in — is unchanged too. Tasks the migration treats as sequential keep their
18
+ * set. The only case that reorders is a set that mixed both kinds: its
19
+ * sequential tasks stay together and the others follow.
20
+ *
21
+ * Generic over the task shape so the serializer (built tasks) and the tests
22
+ * (emitted JSON) apply the one rule.
23
+ */
24
+ export function v31TaskSets(sets, runsSequentially) {
25
+ const out = [];
26
+ for (const set of sets) {
27
+ if (set.length <= 1 || set.every(runsSequentially)) {
28
+ out.push([...set]);
29
+ continue;
30
+ }
31
+ const sequential = set.filter(runsSequentially);
32
+ if (sequential.length)
33
+ out.push(sequential);
34
+ for (const task of set)
35
+ if (!runsSequentially(task))
36
+ out.push([task]);
37
+ }
38
+ return out;
39
+ }
40
+ /** The V30→V31 migration's own test, on emitted task JSON: any rule of any entry condition is `runs-sequentially`. */
41
+ export function taskJsonRunsSequentially(task) {
42
+ return (task.entryConditions ?? []).some((condition) => (condition.rules ?? []).some((group) => group.some((r) => r?.rule === 'runs-sequentially')));
43
+ }
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
+ }